color_debug/
lib.rs

1#![cfg_attr(feature = "nightly", feature(fmt_helpers_for_derive))]
2
3//! Monkey-patches `fmt` machinery to colorize debug output.
4//!
5//! # Usage
6//!
7//! ```rust
8//! unsafe { color_debug::enable() };
9//! ```
10//!
11//! This should only be called once. I make no guarantees about what happens if you call it multiple times.
12
13use std::fmt::{Formatter, Result, Debug};
14
15macro_rules! hook {
16	($var:ident : $t:ty, $a:expr, $b:expr) => {
17		{
18			#[allow(non_upper_case_globals)]
19			static mut $var: $t = $a;
20			static HOOK: $t = $b;
21			let detour = retour::RawDetour::new($var as *const (), HOOK as *const ()).unwrap();
22			detour.enable().unwrap();
23			$var = std::mem::transmute::<*const (), $t>(detour.trampoline());
24			std::mem::forget(detour);
25		}
26	}
27}
28
29macro_rules! hook_fmt {
30	($color:literal $(,$ty:ty)*) => {
31		$(hook! {
32			func: for<'a, 'b, 'c> fn(&$ty, &'b mut Formatter<'c>) -> Result,
33			<$ty as Debug>::fmt,
34			|this, fmt| {
35				color(fmt, $color)?;
36				unsafe { func(this, fmt)? };
37				uncolor(fmt)?;
38				Ok(())
39			}
40		})*
41	}
42}
43
44// For integer types, the Debug formatter is inlined awkwardly, so we need to hook references too.
45macro_rules! hook_fmt_ref {
46	($color:literal $(,$ty:ty)*) => {
47		hook_fmt!($color $(,$ty)*);
48		hook_fmt!($color $(,&'static $ty)*);
49		hook_fmt!($color $(,&'static &'static $ty)*);
50	}
51}
52
53/// Enable colored debug output.
54///
55/// # Safety
56/// Must only be called once, probably. Really, not sure.
57pub unsafe fn enable() {
58	unsafe {
59		hook_fmt_ref! { 1, u8, u16, u32, u64, u128 };
60		hook_fmt_ref! { 1, i8, i16, i32, i64, i128 };
61		hook_fmt! { 1, f32, f64 };
62		hook_fmt! { 1, bool };
63		hook_fmt! { 2, char, str };
64
65		hook! {
66			hook: for<'a> fn(&'a mut std::fmt::DebugStruct<'static, 'static>, &str, &dyn Debug) -> &'a mut std::fmt::DebugStruct<'static, 'static>,
67			std::fmt::DebugStruct::field,
68			|fmt, name, val| unsafe { hook(fmt, &colored(name, 5), val) }
69		};
70
71		// Coloring struct/tuple names can only be reliably done on nightly, since derived impls have shorthand functions.
72		// Unit variants unfortunately use write_str, which I can't hook.
73		#[cfg(feature = "nightly")]
74		structs();
75		#[cfg(feature = "nightly")]
76		tuples();
77	}
78}
79
80#[cfg(feature = "nightly")]
81unsafe fn structs() {
82	macro_rules! hook_struct {
83		($name:ident $(,$a:ident $b:ident)*) => {
84			hook! {
85				func: fn(&mut Formatter<'static>, &str $(, $a: &str, $b: &dyn Debug)*) -> Result,
86				Formatter::$name,
87				|fmt, name $(, $a, $b)*| unsafe { func(fmt, &colored(name, 4) $(, $a, $b)*) }
88			}
89		}
90	}
91
92	hook! {
93		hook: for<'b> fn(&'b mut Formatter<'static>, &str) -> std::fmt::DebugStruct<'b, 'static>,
94		Formatter::debug_struct,
95		|fmt, name| unsafe { hook(fmt, &colored(name, 4)) }
96	};
97
98	hook_struct!(debug_struct_field1_finish, n1 v1);
99	hook_struct!(debug_struct_field2_finish, n1 v1, n2 v2);
100	hook_struct!(debug_struct_field3_finish, n1 v1, n2 v2, n3 v3);
101	hook_struct!(debug_struct_field4_finish, n1 v1, n2 v2, n3 v3, n4 v4);
102	hook_struct!(debug_struct_field5_finish, n1 v1, n2 v2, n3 v3, n4 v4, n5 v5);
103
104	hook! {
105		hook: for<'b> fn(&'b mut Formatter<'static>, &str, &[&str], &[&dyn Debug]) -> Result,
106		Formatter::debug_struct_fields_finish,
107		|fmt, name, fields, values| unsafe { hook(fmt, &colored(name, 4), fields, values) }
108	};
109}
110
111#[cfg(feature = "nightly")]
112unsafe fn tuples() {
113	macro_rules! hook_tuple {
114		($name:ident $(,$a:ident)*) => {
115			hook! {
116				func: fn(&mut Formatter<'static>, &str $(, $a: &dyn Debug)*) -> Result,
117				Formatter::$name,
118				|fmt, name $(, $a)*| unsafe { func(fmt, &colored(name, 4) $(, $a)*) }
119			}
120		}
121	}
122
123	hook! {
124		hook: for<'b> fn(&'b mut Formatter<'static>, &str) -> std::fmt::DebugTuple<'b, 'static>,
125		Formatter::debug_tuple,
126		|fmt, name| unsafe { hook(fmt, &colored(name, 4)) }
127	};
128
129	hook_tuple!(debug_tuple_field1_finish, v1);
130	hook_tuple!(debug_tuple_field2_finish, v1, v2);
131	hook_tuple!(debug_tuple_field3_finish, v1, v2, v3);
132	hook_tuple!(debug_tuple_field4_finish, v1, v2, v3, v4);
133	hook_tuple!(debug_tuple_field5_finish, v1, v2, v3, v4, v5);
134
135	hook! {
136		hook: for<'b> fn(&'b mut Formatter<'static>, &str, &[&dyn Debug]) -> Result,
137		Formatter::debug_tuple_fields_finish,
138		|fmt, name, values| unsafe { hook(fmt, &colored(name, 4), values) }
139	};
140}
141
142fn color(fmt: &mut Formatter, color: u8) -> Result {
143	write!(fmt, "\x1b[3{}m", color)
144}
145
146fn uncolor(fmt: &mut Formatter) -> Result {
147	write!(fmt, "\x1b[39m")
148}
149
150fn colored(name: &str, color: u8) -> String {
151	format!("\x1b[3{}m{}\x1b[39m", color, name)
152}
153
154#[test]
155fn test() {
156	unsafe {
157		enable();
158	}
159
160	#[derive(Debug)]
161	#[allow(dead_code)]
162	struct Nested {
163		name: String,
164		age: i32,
165		things: Vec<String>,
166		other: Option<Box<Nested>>,
167	}
168
169	let val = Nested {
170		name: "Alice".to_string(),
171		age: 42,
172		things: vec!["one".to_string(), "two".to_string()],
173		other: Some(Box::new(Nested {
174			name: "Bob".to_string(),
175			age: 24,
176			things: vec!["three".to_string(), "four".to_string()],
177			other: None,
178		})),
179	};
180
181	println!("{val:#?}");
182}