1use core::fmt;
13use core::fmt::Write as _;
14
15use super::Format;
16use super::escape::{HumanEscaper, render_once, 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 render_once(&lazy, |text| match text {
221 Some(text) => write_str_value(w, text),
222 None => {
225 w.write_char('"')?;
226 write!(
227 HumanEscaper {
228 inner: &mut *w,
229 quoted: true,
230 },
231 "{lazy}"
232 )?;
233 w.write_char('"')
234 }
235 })
236 }
237 }
238}
239
240#[cfg_attr(all(feature = "human"), doc = "```")]
248#[cfg_attr(not(all(feature = "human")), doc = "```ignore")]
249#[cfg(feature = "std")]
257#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
258#[non_exhaustive]
259pub enum ColorChoice {
260 #[default]
276 Auto,
277 Always,
279 Never,
281}
282
283#[cfg(feature = "std")]
284impl ColorChoice {
285 #[must_use]
288 pub fn resolve(self, is_terminal: bool) -> bool {
289 self.resolve_with(is_terminal, cfg!(windows), |key| std::env::var_os(key))
290 }
291
292 #[must_use]
294 pub fn enabled_for_stdout(self) -> bool {
295 use std::io::IsTerminal;
296 self.resolve(std::io::stdout().is_terminal())
297 }
298
299 #[must_use]
301 pub fn enabled_for_stderr(self) -> bool {
302 use std::io::IsTerminal;
303 self.resolve(std::io::stderr().is_terminal())
304 }
305
306 fn resolve_with(
307 self,
308 is_terminal: bool,
309 windows: bool,
310 env: impl Fn(&str) -> Option<std::ffi::OsString>,
311 ) -> bool {
312 let set = |key: &str| env(key).is_some_and(|v| !v.is_empty());
313 match self {
314 Self::Always => true,
315 Self::Never => false,
316 Self::Auto => {
317 if set("NO_COLOR") {
318 return false;
319 }
320 if env("CLICOLOR_FORCE").is_some_and(|v| !v.is_empty() && v != "0") {
321 return true;
322 }
323 if !is_terminal || env("TERM").is_some_and(|t| t == "dumb") {
324 return false;
325 }
326 if windows {
327 set("WT_SESSION") || set("TERM") || env("ConEmuANSI").is_some_and(|v| v == "ON")
328 } else {
329 true
330 }
331 }
332 }
333 }
334}
335
336#[cfg(all(test, feature = "std"))]
337mod tests {
338 use super::*;
339 use crate::record::{Field, Metadata};
340
341 fn fmt_one(fmt: HumanFormat, record: &Record<'_>) -> String {
342 let mut s = String::new();
343 fmt.write_record_generic(record, &mut s).unwrap();
344 let mut d = String::new();
345 fmt.write_record(record, &mut d).unwrap();
346 assert_eq!(s, d);
347 s
348 }
349
350 fn field_human(v: Value<'_>) -> String {
351 let fields = [Field::new("v", v)];
352 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
353 let out = fmt_one(HumanFormat::new(), &record);
354 let start = out.find(" v=").unwrap() + 3;
355 out[start..out.len() - 1].to_string()
356 }
357
358 #[test]
359 fn test_human_level_is_uppercase_padded() {
360 let record = Record::new(Metadata::new(Level::Info, "app"), "hi", &[]);
361 assert_eq!(fmt_one(HumanFormat::new(), &record), "INFO [app] hi\n");
362 let record = Record::new(Metadata::new(Level::Error, "app"), "hi", &[]);
363 assert_eq!(fmt_one(HumanFormat::new(), &record), "ERROR [app] hi\n");
364 }
365
366 #[test]
367 fn test_human_empty_target_omits_brackets() {
368 let record = Record::new(Metadata::new(Level::Info, ""), "hi", &[]);
369 assert_eq!(
370 fmt_one(HumanFormat::new(), &record),
371 "INFO hi
372"
373 );
374 }
375
376 #[test]
377 fn test_human_target_can_be_hidden() {
378 let record = Record::new(Metadata::new(Level::Warn, "app"), "hi", &[]);
379 assert_eq!(
380 fmt_one(HumanFormat::new().show_target(false), &record),
381 "WARN hi\n"
382 );
383 }
384
385 #[test]
386 fn test_human_source_location_optional() {
387 let metadata = Metadata::new(Level::Info, "app").with_location("src/lib.rs", 42);
388 let record = Record::new(metadata, "ok", &[]);
389 let with = fmt_one(HumanFormat::new().show_source_location(true), &record);
390 let without = fmt_one(HumanFormat::new(), &record);
391 assert_eq!(with, "INFO [app] ok (src/lib.rs:42)\n");
392 assert_eq!(without, "INFO [app] ok\n");
393 }
394
395 #[test]
396 fn test_human_timestamp_prepended_when_set() {
397 let metadata = Metadata::new(Level::Info, "app").with_timestamp(1_700_000_000_000_000_000);
398 let record = Record::new(metadata, "ok", &[]);
399 assert_eq!(
400 fmt_one(HumanFormat::new(), &record),
401 "2023-11-14T22:13:20.000000000Z INFO [app] ok\n"
402 );
403 }
404
405 #[test]
406 fn test_human_values_render_per_variant() {
407 assert_eq!(field_human(Value::Null), "<null>");
408 assert_eq!(field_human(Value::Bool(false)), "false");
409 assert_eq!(field_human(Value::I64(-4)), "-4");
410 assert_eq!(field_human(Value::U64(4)), "4");
411 assert_eq!(field_human(Value::I128(-5)), "-5");
412 assert_eq!(field_human(Value::U128(5)), "5");
413 assert_eq!(field_human(Value::F64(2.0)), "2.0");
414 assert_eq!(field_human(Value::F64(f64::NAN)), "NaN");
415 assert_eq!(field_human(Value::Str("")), "\"\"");
416 assert_eq!(field_human(Value::Str("plain")), "plain");
417 assert_eq!(field_human(Value::Str("two words")), "\"two words\"");
418 assert_eq!(field_human(Value::Str("a\"b")), "\"a\\\"b\"");
419 assert_eq!(field_human(Value::Str("a\\b c")), "\"a\\\\b c\"");
420 assert_eq!(field_human(Value::Str("a=b")), "\"a=b\"");
421 assert_eq!(field_human(Value::Str("x\u{1b}y")), "\"x\\x1by\"");
422 assert_eq!(field_human(Value::Char('\n')), "\"\\n\"");
423 assert_eq!(field_human(Value::Char('z')), "z");
424 assert_eq!(field_human(Value::Bytes(&[])), "\"\"");
425 assert_eq!(field_human(Value::Bytes(&[0xab])), "ab");
426 assert_eq!(field_human(Value::Display(&7)), "7");
427 assert_eq!(field_human(Value::Display(&"a b")), "\"a b\"");
428 assert_eq!(field_human(Value::Debug(&"q")), "\"\\\"q\\\"\"");
429 let e = std::io::Error::other("bad\nthing");
430 assert_eq!(field_human(Value::Error(&e)), "\"bad\\nthing\"");
431 }
432
433 #[test]
434 fn test_human_injection_is_neutralized() {
435 let fields = [
437 Field::new("c", Value::Char('\n')),
438 Field::new("k\u{1b}[2J", 1_u32),
439 ];
440 let record = Record::new(
441 Metadata::new(Level::Info, "t\u{1b}]0;pwn\u{7}"),
442 "msg \u{1b}[31mRED\u{b}VT\u{2028}LS\u{85}NEL",
443 &fields,
444 );
445 let out = fmt_one(HumanFormat::new(), &record);
446 assert_eq!(out.matches('\n').count(), 1, "{out:?}");
447 assert!(!out.contains('\u{1b}'), "{out:?}");
448 assert!(!out.contains('\u{7}'), "{out:?}");
449 assert!(!out.contains('\u{2028}'), "{out:?}");
450 assert!(!out.contains('\u{85}'), "{out:?}");
451 assert_eq!(
452 out,
453 "INFO [t\\x1b]0;pwn\\x07] msg \\x1b[31mRED\\x0bVT\\u{2028}LS\\u{85}NEL \
454 c=\"\\n\" k_[2J=1\n"
455 );
456 }
457
458 #[test]
459 fn test_human_color_wraps_level_and_target() {
460 let record = Record::new(Metadata::new(Level::Error, "app"), "x", &[]);
461 let out = fmt_one(HumanFormat::new().color(true), &record);
462 assert_eq!(out, "\x1b[31mERROR\x1b[0m \x1b[2m[app]\x1b[0m x\n");
463 assert!(HumanFormat::new().color(true).is_color());
464 assert!(!HumanFormat::new().is_color());
465 for (level, code) in [
466 (Level::Warn, "\x1b[33m"),
467 (Level::Info, "\x1b[32m"),
468 (Level::Debug, "\x1b[34m"),
469 (Level::Trace, "\x1b[35m"),
470 ] {
471 let record = Record::new(Metadata::new(level, "a"), "x", &[]);
472 let out = fmt_one(HumanFormat::new().color(true).show_target(false), &record);
473 assert!(out.starts_with(code), "{out:?}");
474 }
475 }
476
477 fn env_of(
478 vars: &'static [(&'static str, &'static str)],
479 ) -> impl Fn(&str) -> Option<std::ffi::OsString> {
480 move |key| {
481 vars.iter()
482 .find(|(k, _)| *k == key)
483 .map(|(_, v)| std::ffi::OsString::from(v))
484 }
485 }
486
487 #[test]
488 fn test_color_choice_explicit_choices() {
489 assert!(ColorChoice::Always.resolve_with(false, false, env_of(&[])));
490 assert!(!ColorChoice::Never.resolve_with(true, false, env_of(&[])));
491 }
492
493 #[test]
494 fn test_color_choice_auto_unix_rules() {
495 let auto = ColorChoice::Auto;
496 assert!(auto.resolve_with(true, false, env_of(&[])));
497 assert!(!auto.resolve_with(false, false, env_of(&[])));
498 assert!(!auto.resolve_with(true, false, env_of(&[("NO_COLOR", "1")])));
499 assert!(auto.resolve_with(true, false, env_of(&[("NO_COLOR", "")])));
500 assert!(auto.resolve_with(false, false, env_of(&[("CLICOLOR_FORCE", "1")])));
501 assert!(!auto.resolve_with(false, false, env_of(&[("CLICOLOR_FORCE", "0")])));
502 assert!(!auto.resolve_with(
503 false,
504 false,
505 env_of(&[("CLICOLOR_FORCE", "1"), ("NO_COLOR", "1")])
506 ));
507 assert!(!auto.resolve_with(true, false, env_of(&[("TERM", "dumb")])));
508 }
509
510 #[test]
511 fn test_color_choice_auto_windows_rules() {
512 let auto = ColorChoice::Auto;
513 assert!(!auto.resolve_with(true, true, env_of(&[])));
514 assert!(auto.resolve_with(true, true, env_of(&[("WT_SESSION", "abc")])));
515 assert!(auto.resolve_with(true, true, env_of(&[("TERM", "xterm-256color")])));
516 assert!(auto.resolve_with(true, true, env_of(&[("ConEmuANSI", "ON")])));
517 assert!(!auto.resolve_with(false, true, env_of(&[("WT_SESSION", "abc")])));
518 }
519
520 #[test]
521 fn test_color_choice_default_is_auto() {
522 assert_eq!(ColorChoice::default(), ColorChoice::Auto);
523 assert!(ColorChoice::Always.enabled_for_stdout());
526 assert!(ColorChoice::Always.enabled_for_stderr());
527 assert!(!ColorChoice::Never.enabled_for_stdout());
528 assert!(!ColorChoice::Never.enabled_for_stderr());
529 assert!(ColorChoice::Always.resolve(false));
530 assert!(!ColorChoice::Never.resolve(true));
531 }
532}