1#[cfg(feature = "color")]
12use console::Style;
13use rkyv::{
14 Place, SerializeUnsized,
15 rancor::{Fallible, Source},
16 string::{ArchivedString, StringResolver},
17 with::{ArchiveWith, DeserializeWith, SerializeWith},
18};
19#[cfg(feature = "trace")]
20use tracing::Level;
21
22#[cfg(feature = "color")]
23#[cfg(feature = "color")]
38#[derive(Clone, Copy, Debug)]
39pub struct StyleWith;
40
41#[cfg(feature = "color")]
42fn style_to_dotted(style: &Style) -> String {
48 #[allow(clippy::items_after_statements)]
49 let raw = style.clone().force_styling(true).apply_to("").to_string();
50 if raw.is_empty() {
51 return String::new();
52 }
53 let mut parts: Vec<String> = Vec::new();
54 let mut chars = raw.chars();
55 while let Some(ch) = chars.next() {
56 if ch != '\x1b' {
57 continue;
58 }
59 if chars.next() != Some('[') {
60 continue;
61 }
62 let mut code = String::new();
63 for c in chars.by_ref() {
64 if c == 'm' {
65 break;
66 }
67 code.push(c);
68 }
69 if code == "0" {
71 break;
72 }
73 push_dotted_parts(&code, &mut parts);
74 }
75 parts.join(".")
76}
77
78#[cfg(feature = "color")]
79fn push_dotted_parts(code: &str, parts: &mut Vec<String>) {
82 #[allow(clippy::items_after_statements)]
83 let segs: Vec<&str> = code.split(';').collect();
84 match segs.as_slice() {
85 [n_str] => {
87 let Ok(n) = n_str.parse::<u8>() else { return };
88 match n {
89 1..=9 => {
91 const ATTRS: [&str; 9] = [
92 "bold",
93 "dim",
94 "italic",
95 "underlined",
96 "blink",
97 "blink_fast",
98 "reverse",
99 "hidden",
100 "strikethrough",
101 ];
102 parts.push(ATTRS[(n - 1) as usize].to_string());
103 }
104 30..=37 => {
106 const FG: [&str; 8] = [
107 "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white",
108 ];
109 parts.push(FG[(n - 30) as usize].to_string());
110 }
111 40..=47 => {
113 const BG: [&str; 8] = [
114 "on_black",
115 "on_red",
116 "on_green",
117 "on_yellow",
118 "on_blue",
119 "on_magenta",
120 "on_cyan",
121 "on_white",
122 ];
123 parts.push(BG[(n - 40) as usize].to_string());
124 }
125 _ => {}
126 }
127 }
128 ["38", "5", n_str] => {
132 if let Ok(n) = n_str.parse::<u8>() {
133 parts.push(n.to_string());
134 }
135 }
136 ["38", "2", r_str, g_str, b_str] => {
138 if let (Ok(r), Ok(g), Ok(b)) = (
139 r_str.parse::<u8>(),
140 g_str.parse::<u8>(),
141 b_str.parse::<u8>(),
142 ) {
143 parts.push(format!("#{r:02X}{g:02X}{b:02X}"));
144 }
145 }
146 ["48", "5", n_str] => {
148 if let Ok(n) = n_str.parse::<u8>() {
149 parts.push(format!("on_{n}"));
150 }
151 }
152 ["48", "2", r_str, g_str, b_str] => {
154 if let (Ok(r), Ok(g), Ok(b)) = (
155 r_str.parse::<u8>(),
156 g_str.parse::<u8>(),
157 b_str.parse::<u8>(),
158 ) {
159 parts.push(format!("on_#{r:02X}{g:02X}{b:02X}"));
160 }
161 }
162 _ => {}
163 }
164}
165
166#[cfg(feature = "color")]
167impl ArchiveWith<Style> for StyleWith {
168 type Archived = ArchivedString;
169 type Resolver = StringResolver;
170
171 fn resolve_with(field: &Style, resolver: Self::Resolver, out: Place<Self::Archived>) {
172 ArchivedString::resolve_from_str(&style_to_dotted(field), resolver, out);
173 }
174}
175
176#[cfg(feature = "color")]
177impl<S> SerializeWith<Style, S> for StyleWith
178where
179 S: Fallible + ?Sized,
180 S::Error: Source,
181 str: SerializeUnsized<S>,
182{
183 fn serialize_with(field: &Style, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
184 ArchivedString::serialize_from_str(&style_to_dotted(field), serializer)
185 }
186}
187
188#[cfg(feature = "color")]
189impl<D> DeserializeWith<ArchivedString, Style, D> for StyleWith
190where
191 D: Fallible + ?Sized,
192{
193 fn deserialize_with(field: &ArchivedString, _: &mut D) -> Result<Style, D::Error> {
194 Ok(Style::from_dotted_str(field.as_str()))
195 }
196}
197
198#[cfg(feature = "trace")]
207#[derive(Clone, Copy, Debug)]
208pub(crate) struct LevelWith;
209
210#[cfg(feature = "trace")]
211impl ArchiveWith<Level> for LevelWith {
212 type Archived = ArchivedString;
213 type Resolver = StringResolver;
214
215 fn resolve_with(field: &Level, resolver: Self::Resolver, out: Place<Self::Archived>) {
216 ArchivedString::resolve_from_str(field.as_str(), resolver, out);
217 }
218}
219
220#[cfg(feature = "trace")]
221impl<S> SerializeWith<Level, S> for LevelWith
222where
223 S: Fallible + ?Sized,
224 S::Error: Source,
225 str: SerializeUnsized<S>,
226{
227 fn serialize_with(field: &Level, serializer: &mut S) -> Result<Self::Resolver, S::Error> {
228 ArchivedString::serialize_from_str(field.as_str(), serializer)
229 }
230}
231
232#[cfg(feature = "trace")]
233impl<D: Fallible + ?Sized> DeserializeWith<ArchivedString, Level, D> for LevelWith {
234 fn deserialize_with(field: &ArchivedString, _: &mut D) -> Result<Level, D::Error> {
235 Ok(match field.as_str() {
236 "TRACE" => Level::TRACE,
237 "DEBUG" => Level::DEBUG,
238 "WARN" => Level::WARN,
239 "ERROR" => Level::ERROR,
240 _ => Level::INFO,
241 })
242 }
243}
244
245#[cfg(test)]
248mod tests {
249 #[cfg(feature = "color")]
250 use super::style_to_dotted;
251 #[cfg(feature = "color")]
252 use console::Style;
253
254 #[cfg(feature = "color")]
255 #[test]
256 fn empty_style_round_trips() {
257 let s = Style::new();
258 assert_eq!(style_to_dotted(&s), "");
259 let _ = Style::from_dotted_str(&style_to_dotted(&s));
260 }
261
262 #[cfg(feature = "color")]
263 #[test]
264 fn basic_fg_color_round_trips() {
265 for (style, expected) in [
266 (Style::new().black(), "black"),
267 (Style::new().red(), "red"),
268 (Style::new().green(), "green"),
269 (Style::new().yellow(), "yellow"),
270 (Style::new().blue(), "blue"),
271 (Style::new().magenta(), "magenta"),
272 (Style::new().cyan(), "cyan"),
273 (Style::new().white(), "white"),
274 ] {
275 assert_eq!(style_to_dotted(&style), expected);
276 }
277 }
278
279 #[cfg(feature = "color")]
280 #[test]
281 fn basic_bg_color_round_trips() {
282 assert_eq!(style_to_dotted(&Style::new().on_red()), "on_red");
283 assert_eq!(style_to_dotted(&Style::new().on_blue()), "on_blue");
284 }
285
286 #[cfg(feature = "color")]
287 #[test]
288 fn attrs_round_trips() {
289 assert_eq!(style_to_dotted(&Style::new().bold()), "bold");
290 assert_eq!(style_to_dotted(&Style::new().underlined()), "underlined");
291 assert_eq!(style_to_dotted(&Style::new().italic()), "italic");
292 assert_eq!(
293 style_to_dotted(&Style::new().strikethrough()),
294 "strikethrough"
295 );
296 }
297
298 #[cfg(feature = "color")]
299 #[test]
300 fn compound_style_round_trips() {
301 let s = Style::new().bold().green();
303 let dotted = style_to_dotted(&s);
304 assert!(dotted.contains("green"));
305 assert!(dotted.contains("bold"));
306 let restored = Style::from_dotted_str(&dotted);
307 assert_eq!(
309 s.force_styling(true).apply_to("x").to_string(),
310 restored.force_styling(true).apply_to("x").to_string(),
311 );
312 }
313
314 #[cfg(feature = "color")]
315 #[test]
316 fn true_color_round_trips() {
317 let s = Style::new().true_color(0xFF, 0x00, 0x80);
318 let dotted = style_to_dotted(&s);
319 assert_eq!(dotted, "#FF0080");
320 let restored = Style::from_dotted_str(&dotted);
321 assert_eq!(
322 s.force_styling(true).apply_to("x").to_string(),
323 restored.force_styling(true).apply_to("x").to_string(),
324 );
325 }
326
327 #[cfg(feature = "color")]
328 #[test]
329 fn color256_round_trips() {
330 let s = Style::new().color256(200);
331 let dotted = style_to_dotted(&s);
332 assert_eq!(dotted, "200");
333 let restored = Style::from_dotted_str(&dotted);
334 assert_eq!(
335 s.force_styling(true).apply_to("x").to_string(),
336 restored.force_styling(true).apply_to("x").to_string(),
337 );
338 }
339
340 #[cfg(feature = "trace")]
341 #[test]
342 fn level_as_str_round_trips() {
343 use tracing::Level;
344 for (level, expected) in [
345 (Level::TRACE, "TRACE"),
346 (Level::DEBUG, "DEBUG"),
347 (Level::INFO, "INFO"),
348 (Level::WARN, "WARN"),
349 (Level::ERROR, "ERROR"),
350 ] {
351 assert_eq!(level.as_str(), expected);
352 }
353 }
354
355 #[cfg(feature = "trace")]
356 #[test]
357 fn level_default_fallback() {
358 use super::LevelWith;
359 use tracing::Level;
360 let levels = [
361 Level::TRACE,
362 Level::DEBUG,
363 Level::INFO,
364 Level::WARN,
365 Level::ERROR,
366 ];
367 for level in levels {
368 assert_eq!(level.as_str().parse::<Level>().unwrap(), level);
369 }
370 let _ = LevelWith;
371 }
372
373 #[cfg(feature = "color")]
376 #[test]
377 fn all_bg_colors_round_trips() {
378 for (style, expected) in [
379 (Style::new().on_black(), "on_black"),
380 (Style::new().on_green(), "on_green"),
381 (Style::new().on_yellow(), "on_yellow"),
382 (Style::new().on_magenta(), "on_magenta"),
383 (Style::new().on_cyan(), "on_cyan"),
384 (Style::new().on_white(), "on_white"),
385 ] {
386 assert_eq!(style_to_dotted(&style), expected);
387 }
388 }
389
390 #[cfg(feature = "color")]
391 #[test]
392 fn remaining_attrs_round_trips() {
393 for (style, expected) in [
394 (Style::new().dim(), "dim"),
395 (Style::new().blink(), "blink"),
396 (Style::new().blink_fast(), "blink_fast"),
397 (Style::new().reverse(), "reverse"),
398 (Style::new().hidden(), "hidden"),
399 ] {
400 assert_eq!(style_to_dotted(&style), expected);
401 }
402 }
403
404 #[cfg(feature = "color")]
405 #[test]
406 fn bg_color256_round_trips() {
407 let s = Style::new().on_color256(196);
408 let dotted = style_to_dotted(&s);
409 assert_eq!(dotted, "on_196");
410 let restored = Style::from_dotted_str(&dotted);
411 assert_eq!(
412 s.force_styling(true).apply_to("x").to_string(),
413 restored.force_styling(true).apply_to("x").to_string(),
414 );
415 }
416
417 #[cfg(feature = "color")]
418 #[test]
419 fn bg_true_color_round_trips() {
420 let s = Style::new().on_true_color(0x12, 0x34, 0x56);
421 let dotted = style_to_dotted(&s);
422 assert_eq!(dotted, "on_#123456");
423 let restored = Style::from_dotted_str(&dotted);
424 assert_eq!(
425 s.force_styling(true).apply_to("x").to_string(),
426 restored.force_styling(true).apply_to("x").to_string(),
427 );
428 }
429
430 #[cfg(feature = "color")]
433 #[test]
434 fn push_dotted_parts_unrecognised_codes_are_ignored() {
435 use super::push_dotted_parts;
436
437 let mut parts: Vec<String> = Vec::new();
438
439 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:?}");
445
446 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:?}");
452 }
453
454 #[cfg(feature = "color")]
460 #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
461 struct StyleWrap {
462 #[rkyv(with = rkyv::with::Map<super::StyleWith>)]
463 style: Option<Style>,
464 }
465
466 #[cfg(feature = "color")]
467 #[test]
468 fn style_with_rkyv_round_trip_some() {
469 let original = StyleWrap {
470 style: Some(Style::new().bold().red()),
471 };
472 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&original).unwrap();
473 let restored = rkyv::from_bytes::<StyleWrap, rkyv::rancor::Error>(&bytes).unwrap();
474 let orig_ansi = original
475 .style
476 .as_ref()
477 .unwrap()
478 .clone()
479 .force_styling(true)
480 .apply_to("x")
481 .to_string();
482 let rest_ansi = restored
483 .style
484 .as_ref()
485 .unwrap()
486 .clone()
487 .force_styling(true)
488 .apply_to("x")
489 .to_string();
490 assert_eq!(orig_ansi, rest_ansi);
491 }
492
493 #[cfg(feature = "color")]
494 #[test]
495 fn style_with_rkyv_round_trip_none() {
496 let original = StyleWrap { style: None };
497 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&original).unwrap();
498 let restored = rkyv::from_bytes::<StyleWrap, rkyv::rancor::Error>(&bytes).unwrap();
499 assert!(restored.style.is_none());
500 }
501
502 #[cfg(feature = "trace")]
509 #[derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
510 struct LevelWrap {
511 #[rkyv(with = super::LevelWith)]
512 level: tracing::Level,
513 }
514
515 #[cfg(feature = "trace")]
516 #[test]
517 fn level_with_rkyv_round_trip() {
518 use tracing::Level;
519 for level in [
520 Level::TRACE,
521 Level::DEBUG,
522 Level::INFO, Level::WARN,
524 Level::ERROR,
525 ] {
526 let original = LevelWrap { level };
527 let bytes = rkyv::to_bytes::<rkyv::rancor::Error>(&original).unwrap();
528 let restored = rkyv::from_bytes::<LevelWrap, rkyv::rancor::Error>(&bytes).unwrap();
529 assert_eq!(restored.level, level);
530 }
531 }
532
533 #[cfg(feature = "color")]
536 #[test]
537 fn style_with_clone_and_debug() {
538 use super::StyleWith;
539 let sw = StyleWith;
540 let cloned = sw;
541 let _unused = format!("{cloned:?}");
542 }
543
544 #[cfg(feature = "trace")]
545 #[test]
546 fn level_with_clone_and_debug() {
547 use super::LevelWith;
548 let lw = LevelWith;
549 let cloned = lw;
550 let _unused = format!("{cloned:?}");
551 }
552}