#[cfg(feature = "color")]
use console::Style;
use rkyv::{
Place, SerializeUnsized,
rancor::{Fallible, Source},
string::{ArchivedString, StringResolver},
with::{ArchiveWith, DeserializeWith, SerializeWith},
};
#[cfg(feature = "trace")]
use tracing::Level;
#[cfg(feature = "color")]
#[cfg(feature = "color")]
#[derive(Clone, Copy, Debug)]
pub struct StyleWith;
#[cfg(feature = "color")]
fn style_to_dotted(style: &Style) -> String {
#[allow(clippy::items_after_statements)]
let raw = style.clone().force_styling(true).apply_to("").to_string();
if raw.is_empty() {
return String::new();
}
let mut parts: Vec<String> = Vec::new();
let mut chars = raw.chars();
while let Some(ch) = chars.next() {
if ch != '\x1b' {
continue;
}
if chars.next() != Some('[') {
continue;
}
let mut code = String::new();
for c in chars.by_ref() {
if c == 'm' {
break;
}
code.push(c);
}
if code == "0" {
break;
}
push_dotted_parts(&code, &mut parts);
}
parts.join(".")
}
#[cfg(feature = "color")]
fn push_dotted_parts(code: &str, parts: &mut Vec<String>) {
#[allow(clippy::items_after_statements)]
let segs: Vec<&str> = code.split(';').collect();
match segs.as_slice() {
[n_str] => {
let Ok(n) = n_str.parse::<u8>() else { return };
match n {
1..=9 => {
const ATTRS: [&str; 9] = [
"bold",
"dim",
"italic",
"underlined",
"blink",
"blink_fast",
"reverse",
"hidden",
"strikethrough",
];
parts.push(ATTRS[(n - 1) as usize].to_string());
}
30..=37 => {
const FG: [&str; 8] = [
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
];
parts.push(FG[(n - 30) as usize].to_string());
}
40..=47 => {
const BG: [&str; 8] = [
"on_black",
"on_red",
"on_green",
"on_yellow",
"on_blue",
"on_magenta",
"on_cyan",
"on_white",
];
parts.push(BG[(n - 40) as usize].to_string());
}
_ => {}
}
}
["38", "5", n_str] => {
if let Ok(n) = n_str.parse::<u8>() {
parts.push(n.to_string());
}
}
["38", "2", r_str, g_str, b_str] => {
if let (Ok(r), Ok(g), Ok(b)) = (
r_str.parse::<u8>(),
g_str.parse::<u8>(),
b_str.parse::<u8>(),
) {
parts.push(format!("#{r:02X}{g:02X}{b:02X}"));
}
}
["48", "5", n_str] => {
if let Ok(n) = n_str.parse::<u8>() {
parts.push(format!("on_{n}"));
}
}
["48", "2", r_str, g_str, b_str] => {
if let (Ok(r), Ok(g), Ok(b)) = (
r_str.parse::<u8>(),
g_str.parse::<u8>(),
b_str.parse::<u8>(),
) {
parts.push(format!("on_#{r:02X}{g:02X}{b:02X}"));
}
}
_ => {}
}
}
#[cfg(feature = "color")]
impl ArchiveWith<Style> for StyleWith {
type Archived = ArchivedString;
type Resolver = StringResolver;
fn resolve_with(field: &Style, resolver: Self::Resolver, out: Place<Self::Archived>) {
ArchivedString::resolve_from_str(&style_to_dotted(field), resolver, out);
}
}
#[cfg(feature = "color")]
impl<S> SerializeWith<Style, S> for StyleWith
where
S: Fallible + ?Sized,
S::Error: Source,
str: SerializeUnsized<S>,
{
fn serialize_with(field: &Style, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
ArchivedString::serialize_from_str(&style_to_dotted(field), serializer)
}
}
#[cfg(feature = "color")]
impl<D> DeserializeWith<ArchivedString, Style, D> for StyleWith
where
D: Fallible + ?Sized,
{
fn deserialize_with(field: &ArchivedString, _: &mut D) -> Result<Style, D::Error> {
Ok(Style::from_dotted_str(field.as_str()))
}
}
#[cfg(feature = "trace")]
#[derive(Clone, Copy, Debug)]
pub(crate) struct LevelWith;
#[cfg(feature = "trace")]
impl ArchiveWith<Level> for LevelWith {
type Archived = ArchivedString;
type Resolver = StringResolver;
fn resolve_with(field: &Level, resolver: Self::Resolver, out: Place<Self::Archived>) {
ArchivedString::resolve_from_str(field.as_str(), resolver, out);
}
}
#[cfg(feature = "trace")]
impl<S> SerializeWith<Level, S> for LevelWith
where
S: Fallible + ?Sized,
S::Error: Source,
str: SerializeUnsized<S>,
{
fn serialize_with(field: &Level, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
ArchivedString::serialize_from_str(field.as_str(), serializer)
}
}
#[cfg(feature = "trace")]
impl<D: Fallible + ?Sized> DeserializeWith<ArchivedString, Level, D> for LevelWith {
fn deserialize_with(field: &ArchivedString, _: &mut D) -> Result<Level, D::Error> {
Ok(match field.as_str() {
"TRACE" => Level::TRACE,
"DEBUG" => Level::DEBUG,
"WARN" => Level::WARN,
"ERROR" => Level::ERROR,
_ => Level::INFO,
})
}
}
#[cfg(test)]
mod tests {
#[cfg(feature = "color")]
use super::style_to_dotted;
#[cfg(feature = "color")]
use console::Style;
#[cfg(feature = "color")]
#[test]
fn empty_style_round_trips() {
let s = Style::new();
assert_eq!(style_to_dotted(&s), "");
let _ = Style::from_dotted_str(&style_to_dotted(&s));
}
#[cfg(feature = "color")]
#[test]
fn basic_fg_color_round_trips() {
for (style, expected) in [
(Style::new().black(), "black"),
(Style::new().red(), "red"),
(Style::new().green(), "green"),
(Style::new().yellow(), "yellow"),
(Style::new().blue(), "blue"),
(Style::new().magenta(), "magenta"),
(Style::new().cyan(), "cyan"),
(Style::new().white(), "white"),
] {
assert_eq!(style_to_dotted(&style), expected);
}
}
#[cfg(feature = "color")]
#[test]
fn basic_bg_color_round_trips() {
assert_eq!(style_to_dotted(&Style::new().on_red()), "on_red");
assert_eq!(style_to_dotted(&Style::new().on_blue()), "on_blue");
}
#[cfg(feature = "color")]
#[test]
fn attrs_round_trips() {
assert_eq!(style_to_dotted(&Style::new().bold()), "bold");
assert_eq!(style_to_dotted(&Style::new().underlined()), "underlined");
assert_eq!(style_to_dotted(&Style::new().italic()), "italic");
assert_eq!(
style_to_dotted(&Style::new().strikethrough()),
"strikethrough"
);
}
#[cfg(feature = "color")]
#[test]
fn compound_style_round_trips() {
let s = Style::new().bold().green();
let dotted = style_to_dotted(&s);
assert!(dotted.contains("green"));
assert!(dotted.contains("bold"));
let restored = Style::from_dotted_str(&dotted);
assert_eq!(
s.force_styling(true).apply_to("x").to_string(),
restored.force_styling(true).apply_to("x").to_string(),
);
}
#[cfg(feature = "color")]
#[test]
fn true_color_round_trips() {
let s = Style::new().true_color(0xFF, 0x00, 0x80);
let dotted = style_to_dotted(&s);
assert_eq!(dotted, "#FF0080");
let restored = Style::from_dotted_str(&dotted);
assert_eq!(
s.force_styling(true).apply_to("x").to_string(),
restored.force_styling(true).apply_to("x").to_string(),
);
}
#[cfg(feature = "color")]
#[test]
fn color256_round_trips() {
let s = Style::new().color256(200);
let dotted = style_to_dotted(&s);
assert_eq!(dotted, "200");
let restored = Style::from_dotted_str(&dotted);
assert_eq!(
s.force_styling(true).apply_to("x").to_string(),
restored.force_styling(true).apply_to("x").to_string(),
);
}
#[cfg(feature = "trace")]
#[test]
fn level_as_str_round_trips() {
use tracing::Level;
for (level, expected) in [
(Level::TRACE, "TRACE"),
(Level::DEBUG, "DEBUG"),
(Level::INFO, "INFO"),
(Level::WARN, "WARN"),
(Level::ERROR, "ERROR"),
] {
assert_eq!(level.as_str(), expected);
}
}
#[cfg(feature = "trace")]
#[test]
fn level_default_fallback() {
use super::LevelWith;
use tracing::Level;
let levels = [
Level::TRACE,
Level::DEBUG,
Level::INFO,
Level::WARN,
Level::ERROR,
];
for level in levels {
assert_eq!(level.as_str().parse::<Level>().unwrap(), level);
}
let _ = LevelWith;
}
#[cfg(feature = "color")]
#[test]
fn all_bg_colors_round_trips() {
for (style, expected) in [
(Style::new().on_black(), "on_black"),
(Style::new().on_green(), "on_green"),
(Style::new().on_yellow(), "on_yellow"),
(Style::new().on_magenta(), "on_magenta"),
(Style::new().on_cyan(), "on_cyan"),
(Style::new().on_white(), "on_white"),
] {
assert_eq!(style_to_dotted(&style), expected);
}
}
#[cfg(feature = "color")]
#[test]
fn remaining_attrs_round_trips() {
for (style, expected) in [
(Style::new().dim(), "dim"),
(Style::new().blink(), "blink"),
(Style::new().blink_fast(), "blink_fast"),
(Style::new().reverse(), "reverse"),
(Style::new().hidden(), "hidden"),
] {
assert_eq!(style_to_dotted(&style), expected);
}
}
#[cfg(feature = "color")]
#[test]
fn bg_color256_round_trips() {
let s = Style::new().on_color256(196);
let dotted = style_to_dotted(&s);
assert_eq!(dotted, "on_196");
let restored = Style::from_dotted_str(&dotted);
assert_eq!(
s.force_styling(true).apply_to("x").to_string(),
restored.force_styling(true).apply_to("x").to_string(),
);
}
#[cfg(feature = "color")]
#[test]
fn bg_true_color_round_trips() {
let s = Style::new().on_true_color(0x12, 0x34, 0x56);
let dotted = style_to_dotted(&s);
assert_eq!(dotted, "on_#123456");
let restored = Style::from_dotted_str(&dotted);
assert_eq!(
s.force_styling(true).apply_to("x").to_string(),
restored.force_styling(true).apply_to("x").to_string(),
);
}
#[cfg(feature = "color")]
#[test]
fn push_dotted_parts_unrecognised_codes_are_ignored() {
use super::push_dotted_parts;
let mut parts: Vec<String> = Vec::new();
push_dotted_parts("10", &mut parts); push_dotted_parts("28", &mut parts); push_dotted_parts("50", &mut parts); assert!(parts.is_empty(), "unexpected parts: {parts:?}");
push_dotted_parts("38;5", &mut parts); push_dotted_parts("99;99;99", &mut parts); push_dotted_parts("38;5;196;extra", &mut parts); assert!(parts.is_empty(), "unexpected parts: {parts:?}");
}
#[cfg(feature = "color")]
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
struct StyleWrap {
#[rkyv(with = rkyv::with::Map<super::StyleWith>)]
style: Option<Style>,
}
#[cfg(feature = "color")]
#[test]
fn style_with_rkyv_round_trip_some() {
let original = StyleWrap {
style: Some(Style::new().bold().red()),
};
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&original).unwrap();
let restored = rkyv::from_bytes::<StyleWrap, rkyv::rancor::Error>(&bytes).unwrap();
let orig_ansi = original
.style
.as_ref()
.unwrap()
.clone()
.force_styling(true)
.apply_to("x")
.to_string();
let rest_ansi = restored
.style
.as_ref()
.unwrap()
.clone()
.force_styling(true)
.apply_to("x")
.to_string();
assert_eq!(orig_ansi, rest_ansi);
}
#[cfg(feature = "color")]
#[test]
fn style_with_rkyv_round_trip_none() {
let original = StyleWrap { style: None };
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&original).unwrap();
let restored = rkyv::from_bytes::<StyleWrap, rkyv::rancor::Error>(&bytes).unwrap();
assert!(restored.style.is_none());
}
#[cfg(feature = "trace")]
#[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
struct LevelWrap {
#[rkyv(with = super::LevelWith)]
level: tracing::Level,
}
#[cfg(feature = "trace")]
#[test]
fn level_with_rkyv_round_trip() {
use tracing::Level;
for level in [
Level::TRACE,
Level::DEBUG,
Level::INFO, Level::WARN,
Level::ERROR,
] {
let original = LevelWrap { level };
let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&original).unwrap();
let restored = rkyv::from_bytes::<LevelWrap, rkyv::rancor::Error>(&bytes).unwrap();
assert_eq!(restored.level, level);
}
}
#[cfg(feature = "color")]
#[test]
fn style_with_clone_and_debug() {
use super::StyleWith;
let sw = StyleWith;
let cloned = sw;
let _unused = format!("{cloned:?}");
}
#[cfg(feature = "trace")]
#[test]
fn level_with_clone_and_debug() {
use super::LevelWith;
let lw = LevelWith;
let cloned = lw;
let _unused = format!("{cloned:?}");
}
}