Is there a simple way to get output like this, without implementing the Debug trait directly on the struct (a Debug trait on another type would be fine)
Yes. #[derive(Debug)] implements Debug by just calling Debug::debug on each of the members in turn. Follow the instructions in How to implement a custom 'fmt::Debug' trait? for your newtype wrapper:
use std::fmt;
#[derive(Debug)]
struct Test {
int: u8,
bits: Bits,
hex: u8,
}
struct Bits(u8);
impl fmt::Debug for Bits {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0b{:08b}", self.0)
}
}
In a different direction, it would not be strange to have a method on your primary struct that returns another struct "for display purposes", akin to std::path::Display. This allows you to move complicated display logic to a separate type while allowing your original struct to not have newtypes that might get in your way:
use std::fmt;
#[derive(Debug)]
struct Test {
int: u8,
bits: u8,
hex: u8,
}
impl Test {
fn pretty_debug(&self) -> PrettyDebug<'_> {
PrettyDebug {
int: &self.int,
bits: Bits(&self.bits),
hex: &self.hex,
}
}
}
#[derive(Debug)]
struct PrettyDebug<'a> {
int: &'a u8,
bits: Bits<'a>,
hex: &'a u8,
}
struct Bits<'a>(&'a u8);
impl fmt::Debug for Bits<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0b{:08b}", self.0)
}
}
It's a little silly to have a reference to a u8, but references are the most generic solution here — choose appropriate data types for your case.
You could also implement Debug directly for your PrettyDebug type:
use std::fmt;
#[derive(Debug)]
struct Test {
int: u8,
bits: u8,
hex: u8,
}
impl Test {
fn pretty_debug(&self) -> PrettyDebug<'_> {
PrettyDebug(self)
}
}
struct PrettyDebug<'a>(&'a Test);
impl fmt::Debug for PrettyDebug<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Test")
.field("int", &self.0.int)
.field("bits", &format_args!("0b{:08b}", self.0.bits))
.field("hex", &format_args!("0x{:02x}", self.0.hex))
.finish()
}
}