Skip to main content

facet_pretty/
printer.rs

1//! Pretty printer implementation for Facet types
2
3use alloc::borrow::Cow;
4use alloc::collections::BTreeMap;
5use core::{
6    fmt::{self, Write},
7    hash::{Hash, Hasher},
8    str,
9};
10use std::{hash::DefaultHasher, sync::LazyLock};
11
12use facet_core::{
13    Def, DynDateTimeKind, DynValueKind, Facet, Field, PointerType, PrimitiveType, PtrUninit,
14    SequenceType, Shape, StructKind, StructType, TextualType, Type, TypeNameOpts, UserType,
15};
16use facet_reflect::{Peek, ValueId};
17
18use owo_colors::{OwoColorize, Rgb};
19
20use crate::color::ColorGenerator;
21use crate::shape::{FieldSpan, Path, PathSegment, Span};
22
23/// Tokyo Night color palette (RGB values from official theme)
24///
25/// See: <https://github.com/tokyo-night/tokyo-night-vscode-theme>
26pub mod tokyo_night {
27    use owo_colors::Rgb;
28
29    // ========================================================================
30    // Core colors
31    // ========================================================================
32
33    /// Foreground - main text (#a9b1d6)
34    pub const FOREGROUND: Rgb = Rgb(169, 177, 214);
35    /// Background (#1a1b26)
36    pub const BACKGROUND: Rgb = Rgb(26, 27, 38);
37    /// Comment - muted text (#565f89)
38    pub const COMMENT: Rgb = Rgb(86, 95, 137);
39
40    // ========================================================================
41    // Terminal ANSI colors
42    // ========================================================================
43
44    /// Black (#414868)
45    pub const BLACK: Rgb = Rgb(65, 72, 104);
46    /// Red (#f7768e)
47    pub const RED: Rgb = Rgb(247, 118, 142);
48    /// Green - teal/cyan green (#73daca)
49    pub const GREEN: Rgb = Rgb(115, 218, 202);
50    /// Yellow - warm orange-yellow (#e0af68)
51    pub const YELLOW: Rgb = Rgb(224, 175, 104);
52    /// Blue (#7aa2f7)
53    pub const BLUE: Rgb = Rgb(122, 162, 247);
54    /// Magenta - purple (#bb9af7)
55    pub const MAGENTA: Rgb = Rgb(187, 154, 247);
56    /// Cyan - bright cyan (#7dcfff)
57    pub const CYAN: Rgb = Rgb(125, 207, 255);
58    /// White - muted white (#787c99)
59    pub const WHITE: Rgb = Rgb(120, 124, 153);
60
61    /// Bright white (#acb0d0)
62    pub const BRIGHT_WHITE: Rgb = Rgb(172, 176, 208);
63
64    // ========================================================================
65    // Extended syntax colors
66    // ========================================================================
67
68    /// Orange - numbers, constants (#ff9e64)
69    pub const ORANGE: Rgb = Rgb(255, 158, 100);
70    /// Dark green - strings (#9ece6a)
71    pub const DARK_GREEN: Rgb = Rgb(158, 206, 106);
72
73    // ========================================================================
74    // Semantic/status colors
75    // ========================================================================
76
77    /// Error - bright red for errors (#db4b4b)
78    pub const ERROR: Rgb = Rgb(219, 75, 75);
79    /// Warning - same as yellow (#e0af68)
80    pub const WARNING: Rgb = YELLOW;
81    /// Info - teal-blue (#0db9d7)
82    pub const INFO: Rgb = Rgb(13, 185, 215);
83    /// Hint - same as comment, muted
84    pub const HINT: Rgb = COMMENT;
85
86    // ========================================================================
87    // Semantic aliases for specific uses
88    // ========================================================================
89
90    /// Type names - blue, bold
91    pub const TYPE_NAME: Rgb = BLUE;
92    /// Field names - green/teal
93    pub const FIELD_NAME: Rgb = GREEN;
94    /// String literals - dark green
95    pub const STRING: Rgb = DARK_GREEN;
96    /// Number literals - orange
97    pub const NUMBER: Rgb = ORANGE;
98    /// Keywords (null, true, false) - magenta
99    pub const KEYWORD: Rgb = MAGENTA;
100    /// Deletions in diffs - red
101    pub const DELETION: Rgb = RED;
102    /// Insertions in diffs - green
103    pub const INSERTION: Rgb = GREEN;
104    /// Muted/unchanged - comment color
105    pub const MUTED: Rgb = COMMENT;
106    /// Borders - very muted, comment color
107    pub const BORDER: Rgb = COMMENT;
108}
109
110/// A formatter for pretty-printing Facet types
111#[derive(Clone, PartialEq)]
112pub struct PrettyPrinter {
113    /// usize::MAX is a special value that means indenting with tabs instead of spaces
114    indent_size: usize,
115    max_depth: Option<usize>,
116    color_generator: ColorGenerator,
117    colors: ColorMode,
118    list_u8_as_bytes: bool,
119    /// Skip type names for Options (show `Some(x)` instead of `Option<T>::Some(x)`)
120    minimal_option_names: bool,
121    /// Whether to show doc comments in output
122    show_doc_comments: bool,
123    /// Maximum length for strings/bytes before truncating the middle (None = no limit)
124    max_content_len: Option<usize>,
125}
126
127impl Default for PrettyPrinter {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl PrettyPrinter {
134    /// Create a new PrettyPrinter with default settings
135    pub const fn new() -> Self {
136        Self {
137            indent_size: 2,
138            max_depth: None,
139            color_generator: ColorGenerator::new(),
140            colors: ColorMode::Auto,
141            list_u8_as_bytes: true,
142            minimal_option_names: false,
143            show_doc_comments: false,
144            max_content_len: None,
145        }
146    }
147
148    /// Set the indentation size
149    pub const fn with_indent_size(mut self, size: usize) -> Self {
150        self.indent_size = size;
151        self
152    }
153
154    /// Set the maximum depth for recursive printing
155    pub const fn with_max_depth(mut self, depth: usize) -> Self {
156        self.max_depth = Some(depth);
157        self
158    }
159
160    /// Set the color generator
161    pub const fn with_color_generator(mut self, generator: ColorGenerator) -> Self {
162        self.color_generator = generator;
163        self
164    }
165
166    /// Enable or disable colors. Use `None` to automatically detect color support based on the `NO_COLOR` environment variable.
167    pub const fn with_colors(mut self, enable_colors: ColorMode) -> Self {
168        self.colors = enable_colors;
169        self
170    }
171
172    /// Use minimal names for Options (show `Some(x)` instead of `Option<T>::Some(x)`)
173    pub const fn with_minimal_option_names(mut self, minimal: bool) -> Self {
174        self.minimal_option_names = minimal;
175        self
176    }
177
178    /// Enable or disable doc comments in output
179    pub const fn with_doc_comments(mut self, show: bool) -> Self {
180        self.show_doc_comments = show;
181        self
182    }
183
184    /// Set the maximum length for strings and byte arrays before truncating
185    ///
186    /// When set, strings and byte arrays longer than this limit will be
187    /// truncated in the middle, showing the beginning and end with `...` between.
188    pub const fn with_max_content_len(mut self, max_len: usize) -> Self {
189        self.max_content_len = Some(max_len);
190        self
191    }
192
193    /// Format a value to a string
194    pub fn format<'a, T: ?Sized + Facet<'a>>(&self, value: &T) -> String {
195        let value = Peek::new(value);
196
197        let mut output = String::new();
198        self.format_peek_internal(value, &mut output, &mut BTreeMap::new())
199            .expect("Formatting failed");
200
201        output
202    }
203
204    /// Format a value to a formatter
205    pub fn format_to<'a, T: ?Sized + Facet<'a>>(
206        &self,
207        value: &T,
208        f: &mut fmt::Formatter<'_>,
209    ) -> fmt::Result {
210        let value = Peek::new(value);
211        self.format_peek_internal(value, f, &mut BTreeMap::new())
212    }
213
214    /// Format a value to a string
215    pub fn format_peek(&self, value: Peek<'_, '_>) -> String {
216        let mut output = String::new();
217        self.format_peek_internal(value, &mut output, &mut BTreeMap::new())
218            .expect("Formatting failed");
219        output
220    }
221
222    pub(crate) fn shape_chunkiness(shape: &Shape) -> usize {
223        let mut shape = shape;
224        while let Type::Pointer(PointerType::Reference(inner)) = shape.ty {
225            shape = inner.target;
226        }
227
228        match shape.ty {
229            Type::Pointer(_) | Type::Primitive(_) => 1,
230            Type::Sequence(SequenceType::Array(ty)) => {
231                Self::shape_chunkiness(ty.t).saturating_mul(ty.n)
232            }
233            Type::Sequence(SequenceType::Slice(_)) => usize::MAX,
234            Type::User(ty) => match ty {
235                UserType::Struct(ty) => {
236                    let mut sum = 0usize;
237                    for field in ty.fields {
238                        sum = sum.saturating_add(Self::shape_chunkiness(field.shape()));
239                    }
240                    sum
241                }
242                UserType::Enum(ty) => {
243                    let mut max = 0usize;
244                    for variant in ty.variants {
245                        max = Ord::max(max, {
246                            let mut sum = 0usize;
247                            for field in variant.data.fields {
248                                sum = sum.saturating_add(Self::shape_chunkiness(field.shape()));
249                            }
250                            sum
251                        })
252                    }
253                    max
254                }
255                UserType::Opaque | UserType::Union(_) => 1,
256            },
257            Type::Undefined => 1,
258        }
259    }
260
261    #[inline]
262    fn use_colors(&self) -> bool {
263        self.colors.enabled()
264    }
265
266    #[allow(clippy::too_many_arguments)]
267    pub(crate) fn format_peek_internal_(
268        &self,
269        value: Peek<'_, '_>,
270        f: &mut dyn Write,
271        visited: &mut BTreeMap<ValueId, usize>,
272        format_depth: usize,
273        type_depth: usize,
274        short: bool,
275    ) -> fmt::Result {
276        let mut value = value;
277        while let Ok(ptr) = value.into_pointer()
278            && let Some(pointee) = ptr.borrow_inner()
279        {
280            value = pointee;
281        }
282
283        // Unwrap transparent wrappers (e.g., newtype wrappers like IntAsString(String))
284        // This matches serialization behavior where we serialize the inner value directly
285        let value = value.innermost_peek();
286        let shape = value.shape();
287
288        if let Some(prev_type_depth) = visited.insert(value.id(), type_depth) {
289            self.write_type_name(f, &value)?;
290            self.write_punctuation(f, " { ")?;
291            self.write_comment(
292                f,
293                &format!(
294                    "/* cycle detected at {} (first seen at type_depth {}) */",
295                    value.id(),
296                    prev_type_depth,
297                ),
298            )?;
299            visited.remove(&value.id());
300            return Ok(());
301        }
302
303        // Handle proxy types by converting to the proxy representation and formatting that
304        if let Some(proxy_def) = shape.proxy {
305            let result = self.format_via_proxy(
306                value,
307                proxy_def,
308                f,
309                visited,
310                format_depth,
311                type_depth,
312                short,
313            );
314
315            visited.remove(&value.id());
316            return result;
317        }
318
319        match (shape.def, shape.ty) {
320            (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
321                let value = value.get::<str>().unwrap();
322                self.format_str_value(f, value)?;
323            }
324            // Handle String specially to add quotes (like &str)
325            (Def::Scalar, _) if value.shape().id == <alloc::string::String as Facet>::SHAPE.id => {
326                let s = value.get::<alloc::string::String>().unwrap();
327                self.format_str_value(f, s)?;
328            }
329            (Def::Scalar, _) => self.format_scalar(value, f)?,
330            (Def::Option(_), _) => {
331                let option = value.into_option().unwrap();
332
333                // Print the Option name (unless minimal mode)
334                if !self.minimal_option_names {
335                    self.write_type_name(f, &value)?;
336                }
337
338                if let Some(inner) = option.value() {
339                    let prefix = if self.minimal_option_names {
340                        "Some("
341                    } else {
342                        "::Some("
343                    };
344                    self.write_punctuation(f, prefix)?;
345                    self.format_peek_internal_(
346                        inner,
347                        f,
348                        visited,
349                        format_depth,
350                        type_depth + 1,
351                        short,
352                    )?;
353                    self.write_punctuation(f, ")")?;
354                } else {
355                    let suffix = if self.minimal_option_names {
356                        "None"
357                    } else {
358                        "::None"
359                    };
360                    self.write_punctuation(f, suffix)?;
361                }
362            }
363
364            (_, Type::Pointer(PointerType::Raw(_) | PointerType::Function(_))) => {
365                self.write_type_name(f, &value)?;
366                let addr = unsafe { value.data().read::<*const ()>() };
367                let value = Peek::new(&addr);
368                self.format_scalar(value, f)?;
369            }
370
371            (_, Type::User(UserType::Union(_))) => {
372                if !short && self.show_doc_comments {
373                    for &line in shape.doc {
374                        self.write_comment(f, &format!("///{line}"))?;
375                        writeln!(f)?;
376                        self.indent(f, format_depth)?;
377                    }
378                }
379                self.write_type_name(f, &value)?;
380
381                self.write_punctuation(f, " { ")?;
382                self.write_comment(f, "/* contents of untagged union */")?;
383                self.write_punctuation(f, " }")?;
384            }
385
386            (
387                _,
388                Type::User(UserType::Struct(
389                    ty @ StructType {
390                        kind: StructKind::Tuple | StructKind::TupleStruct,
391                        ..
392                    },
393                )),
394            ) => {
395                if !short && self.show_doc_comments {
396                    for &line in shape.doc {
397                        self.write_comment(f, &format!("///{line}"))?;
398                        writeln!(f)?;
399                        self.indent(f, format_depth)?;
400                    }
401                }
402
403                self.write_type_name(f, &value)?;
404                if matches!(ty.kind, StructKind::Tuple) {
405                    write!(f, " ")?;
406                }
407                let value = value.into_struct().unwrap();
408
409                let fields = ty.fields;
410                self.format_tuple_fields(
411                    &|i| value.field(i).unwrap(),
412                    f,
413                    visited,
414                    format_depth,
415                    type_depth,
416                    fields,
417                    short,
418                    matches!(ty.kind, StructKind::Tuple),
419                )?;
420            }
421
422            (
423                _,
424                Type::User(UserType::Struct(
425                    ty @ StructType {
426                        kind: StructKind::Struct | StructKind::Unit,
427                        ..
428                    },
429                )),
430            ) => {
431                if !short && self.show_doc_comments {
432                    for &line in shape.doc {
433                        self.write_comment(f, &format!("///{line}"))?;
434                        writeln!(f)?;
435                        self.indent(f, format_depth)?;
436                    }
437                }
438
439                self.write_type_name(f, &value)?;
440
441                if matches!(ty.kind, StructKind::Struct) {
442                    let value = value.into_struct().unwrap();
443                    self.format_struct_fields(
444                        &|i| value.field(i).unwrap(),
445                        f,
446                        visited,
447                        format_depth,
448                        type_depth,
449                        ty.fields,
450                        short,
451                    )?;
452                }
453            }
454
455            (_, Type::User(UserType::Enum(_))) => {
456                let enum_peek = value.into_enum().unwrap();
457                match enum_peek.active_variant() {
458                    Err(_) => {
459                        // Print the enum name
460                        self.write_type_name(f, &value)?;
461                        self.write_punctuation(f, " {")?;
462                        self.write_comment(f, " /* cannot determine variant */ ")?;
463                        self.write_punctuation(f, "}")?;
464                    }
465                    Ok(variant) => {
466                        if !short && self.show_doc_comments {
467                            for &line in shape.doc {
468                                self.write_comment(f, &format!("///{line}"))?;
469                                writeln!(f)?;
470                                self.indent(f, format_depth)?;
471                            }
472                            for &line in variant.doc {
473                                self.write_comment(f, &format!("///{line}"))?;
474                                writeln!(f)?;
475                                self.indent(f, format_depth)?;
476                            }
477                        }
478                        self.write_type_name(f, &value)?;
479                        self.write_punctuation(f, "::")?;
480
481                        // Variant docs are already handled above
482
483                        // Get the active variant name - we've already checked above that we can get it
484                        // This is the same variant, but we're repeating the code here to ensure consistency
485
486                        // Apply color for variant name
487                        if self.use_colors() {
488                            write!(f, "{}", variant.name.bold())?;
489                        } else {
490                            write!(f, "{}", variant.name)?;
491                        }
492
493                        // Process the variant fields based on the variant kind
494                        match variant.data.kind {
495                            StructKind::Unit => {
496                                // Unit variant has no fields, nothing more to print
497                            }
498                            StructKind::Struct => self.format_struct_fields(
499                                &|i| enum_peek.field(i).unwrap().unwrap(),
500                                f,
501                                visited,
502                                format_depth,
503                                type_depth,
504                                variant.data.fields,
505                                short,
506                            )?,
507                            _ => self.format_tuple_fields(
508                                &|i| enum_peek.field(i).unwrap().unwrap(),
509                                f,
510                                visited,
511                                format_depth,
512                                type_depth,
513                                variant.data.fields,
514                                short,
515                                false,
516                            )?,
517                        }
518                    }
519                };
520            }
521
522            _ if value.into_list_like().is_ok() => {
523                let list = value.into_list_like().unwrap();
524
525                // When recursing into a list, always increment format_depth
526                // Only increment type_depth if we're moving to a different address
527
528                // Print the list name
529                self.write_type_name(f, &value)?;
530
531                if !list.is_empty() {
532                    if list.def().t().is_type::<u8>() && self.list_u8_as_bytes {
533                        let total_len = list.len();
534                        let truncate = self.max_content_len.is_some_and(|max| total_len > max);
535
536                        self.write_punctuation(f, " [")?;
537
538                        if truncate {
539                            let max = self.max_content_len.unwrap();
540                            let half = max / 2;
541                            let start_count = half;
542                            let end_count = half;
543
544                            // Show beginning
545                            for (idx, item) in list.iter().enumerate().take(start_count) {
546                                if !short && idx % 16 == 0 {
547                                    writeln!(f)?;
548                                    self.indent(f, format_depth + 1)?;
549                                }
550                                write!(f, " ")?;
551                                let byte = *item.get::<u8>().unwrap();
552                                if self.use_colors() {
553                                    let mut hasher = DefaultHasher::new();
554                                    byte.hash(&mut hasher);
555                                    let hash = hasher.finish();
556                                    let color = self.color_generator.generate_color(hash);
557                                    let rgb = Rgb(color.r, color.g, color.b);
558                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
559                                } else {
560                                    write!(f, "{byte:02x}")?;
561                                }
562                            }
563
564                            // Show ellipsis
565                            let omitted = total_len - start_count - end_count;
566                            if !short {
567                                writeln!(f)?;
568                                self.indent(f, format_depth + 1)?;
569                            }
570                            write!(f, " ...({omitted} bytes)...")?;
571
572                            // Show end
573                            for (idx, item) in list.iter().enumerate().skip(total_len - end_count) {
574                                let display_idx = start_count + 1 + (idx - (total_len - end_count));
575                                if !short && display_idx.is_multiple_of(16) {
576                                    writeln!(f)?;
577                                    self.indent(f, format_depth + 1)?;
578                                }
579                                write!(f, " ")?;
580                                let byte = *item.get::<u8>().unwrap();
581                                if self.use_colors() {
582                                    let mut hasher = DefaultHasher::new();
583                                    byte.hash(&mut hasher);
584                                    let hash = hasher.finish();
585                                    let color = self.color_generator.generate_color(hash);
586                                    let rgb = Rgb(color.r, color.g, color.b);
587                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
588                                } else {
589                                    write!(f, "{byte:02x}")?;
590                                }
591                            }
592                        } else {
593                            for (idx, item) in list.iter().enumerate() {
594                                if !short && idx % 16 == 0 {
595                                    writeln!(f)?;
596                                    self.indent(f, format_depth + 1)?;
597                                }
598                                write!(f, " ")?;
599
600                                let byte = *item.get::<u8>().unwrap();
601                                if self.use_colors() {
602                                    let mut hasher = DefaultHasher::new();
603                                    byte.hash(&mut hasher);
604                                    let hash = hasher.finish();
605                                    let color = self.color_generator.generate_color(hash);
606                                    let rgb = Rgb(color.r, color.g, color.b);
607                                    write!(f, "{}", format!("{byte:02x}").color(rgb))?;
608                                } else {
609                                    write!(f, "{byte:02x}")?;
610                                }
611                            }
612                        }
613
614                        if !short {
615                            writeln!(f)?;
616                            self.indent(f, format_depth)?;
617                        }
618                        self.write_punctuation(f, "]")?;
619                    } else {
620                        // Check if elements are simple scalars - render inline if so
621                        let elem_shape = list.def().t();
622                        let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
623
624                        self.write_punctuation(f, " [")?;
625                        let len = list.len();
626                        for (idx, item) in list.iter().enumerate() {
627                            if !short && !is_simple {
628                                writeln!(f)?;
629                                self.indent(f, format_depth + 1)?;
630                            } else if idx > 0 {
631                                write!(f, " ")?;
632                            }
633                            self.format_peek_internal_(
634                                item,
635                                f,
636                                visited,
637                                format_depth + 1,
638                                type_depth + 1,
639                                short || is_simple,
640                            )?;
641
642                            if (!short && !is_simple) || idx + 1 < len {
643                                self.write_punctuation(f, ",")?;
644                            }
645                        }
646                        if !short && !is_simple {
647                            writeln!(f)?;
648                            self.indent(f, format_depth)?;
649                        }
650                        self.write_punctuation(f, "]")?;
651                    }
652                } else {
653                    self.write_punctuation(f, "[]")?;
654                }
655            }
656
657            _ if value.into_set().is_ok() => {
658                self.write_type_name(f, &value)?;
659
660                let value = value.into_set().unwrap();
661                self.write_punctuation(f, " [")?;
662                if !value.is_empty() {
663                    let len = value.len();
664                    for (idx, item) in value.iter().enumerate() {
665                        if !short {
666                            writeln!(f)?;
667                            self.indent(f, format_depth + 1)?;
668                        }
669                        self.format_peek_internal_(
670                            item,
671                            f,
672                            visited,
673                            format_depth + 1,
674                            type_depth + 1,
675                            short,
676                        )?;
677                        if !short || idx + 1 < len {
678                            self.write_punctuation(f, ",")?;
679                        } else {
680                            write!(f, " ")?;
681                        }
682                    }
683                    if !short {
684                        writeln!(f)?;
685                        self.indent(f, format_depth)?;
686                    }
687                }
688                self.write_punctuation(f, "]")?;
689            }
690
691            (Def::Map(def), _) => {
692                let key_is_short = Self::shape_chunkiness(def.k) <= 2;
693
694                self.write_type_name(f, &value)?;
695
696                let value = value.into_map().unwrap();
697                self.write_punctuation(f, " [")?;
698
699                if !value.is_empty() {
700                    let len = value.len();
701                    for (idx, (key, value)) in value.iter().enumerate() {
702                        if !short {
703                            writeln!(f)?;
704                            self.indent(f, format_depth + 1)?;
705                        }
706                        self.format_peek_internal_(
707                            key,
708                            f,
709                            visited,
710                            format_depth + 1,
711                            type_depth + 1,
712                            key_is_short,
713                        )?;
714                        self.write_punctuation(f, " => ")?;
715                        self.format_peek_internal_(
716                            value,
717                            f,
718                            visited,
719                            format_depth + 1,
720                            type_depth + 1,
721                            short,
722                        )?;
723                        if !short || idx + 1 < len {
724                            self.write_punctuation(f, ",")?;
725                        } else {
726                            write!(f, " ")?;
727                        }
728                    }
729                    if !short {
730                        writeln!(f)?;
731                        self.indent(f, format_depth)?;
732                    }
733                }
734
735                self.write_punctuation(f, "]")?;
736            }
737
738            (Def::DynamicValue(_), _) => {
739                let dyn_val = value.into_dynamic_value().unwrap();
740                match dyn_val.kind() {
741                    DynValueKind::Null => {
742                        self.write_keyword(f, "null")?;
743                    }
744                    DynValueKind::Bool => {
745                        if let Some(b) = dyn_val.as_bool() {
746                            self.write_keyword(f, if b { "true" } else { "false" })?;
747                        }
748                    }
749                    DynValueKind::Number => {
750                        if let Some(n) = dyn_val.as_i64() {
751                            self.format_number(f, &n.to_string())?;
752                        } else if let Some(n) = dyn_val.as_u64() {
753                            self.format_number(f, &n.to_string())?;
754                        } else if let Some(n) = dyn_val.as_f64() {
755                            self.format_number(f, &n.to_string())?;
756                        }
757                    }
758                    DynValueKind::String => {
759                        if let Some(s) = dyn_val.as_str() {
760                            self.format_string(f, s)?;
761                        }
762                    }
763                    DynValueKind::Bytes => {
764                        if let Some(bytes) = dyn_val.as_bytes() {
765                            self.format_bytes(f, bytes)?;
766                        }
767                    }
768                    DynValueKind::Array => {
769                        let len = dyn_val.array_len().unwrap_or(0);
770                        if len == 0 {
771                            self.write_punctuation(f, "[]")?;
772                        } else {
773                            self.write_punctuation(f, "[")?;
774                            for idx in 0..len {
775                                if !short {
776                                    writeln!(f)?;
777                                    self.indent(f, format_depth + 1)?;
778                                }
779                                if let Some(elem) = dyn_val.array_get(idx) {
780                                    self.format_peek_internal_(
781                                        elem,
782                                        f,
783                                        visited,
784                                        format_depth + 1,
785                                        type_depth + 1,
786                                        short,
787                                    )?;
788                                }
789                                if !short || idx + 1 < len {
790                                    self.write_punctuation(f, ",")?;
791                                } else {
792                                    write!(f, " ")?;
793                                }
794                            }
795                            if !short {
796                                writeln!(f)?;
797                                self.indent(f, format_depth)?;
798                            }
799                            self.write_punctuation(f, "]")?;
800                        }
801                    }
802                    DynValueKind::Object => {
803                        let len = dyn_val.object_len().unwrap_or(0);
804                        if len == 0 {
805                            self.write_punctuation(f, "{}")?;
806                        } else {
807                            self.write_punctuation(f, "{")?;
808                            for idx in 0..len {
809                                if !short {
810                                    writeln!(f)?;
811                                    self.indent(f, format_depth + 1)?;
812                                }
813                                if let Some((key, val)) = dyn_val.object_get_entry(idx) {
814                                    self.write_field_name(f, key)?;
815                                    self.write_punctuation(f, ": ")?;
816                                    self.format_peek_internal_(
817                                        val,
818                                        f,
819                                        visited,
820                                        format_depth + 1,
821                                        type_depth + 1,
822                                        short,
823                                    )?;
824                                }
825                                if !short || idx + 1 < len {
826                                    self.write_punctuation(f, ",")?;
827                                } else {
828                                    write!(f, " ")?;
829                                }
830                            }
831                            if !short {
832                                writeln!(f)?;
833                                self.indent(f, format_depth)?;
834                            }
835                            self.write_punctuation(f, "}")?;
836                        }
837                    }
838                    DynValueKind::DateTime => {
839                        // Format datetime using the vtable's get_datetime
840                        #[allow(clippy::uninlined_format_args)]
841                        if let Some((year, month, day, hour, minute, second, nanos, kind)) =
842                            dyn_val.as_datetime()
843                        {
844                            match kind {
845                                DynDateTimeKind::Offset { offset_minutes } => {
846                                    if nanos > 0 {
847                                        write!(
848                                            f,
849                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
850                                            year, month, day, hour, minute, second, nanos
851                                        )?;
852                                    } else {
853                                        write!(
854                                            f,
855                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
856                                            year, month, day, hour, minute, second
857                                        )?;
858                                    }
859                                    if offset_minutes == 0 {
860                                        write!(f, "Z")?;
861                                    } else {
862                                        let sign = if offset_minutes >= 0 { '+' } else { '-' };
863                                        let abs = offset_minutes.abs();
864                                        write!(f, "{}{:02}:{:02}", sign, abs / 60, abs % 60)?;
865                                    }
866                                }
867                                DynDateTimeKind::LocalDateTime => {
868                                    if nanos > 0 {
869                                        write!(
870                                            f,
871                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}",
872                                            year, month, day, hour, minute, second, nanos
873                                        )?;
874                                    } else {
875                                        write!(
876                                            f,
877                                            "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}",
878                                            year, month, day, hour, minute, second
879                                        )?;
880                                    }
881                                }
882                                DynDateTimeKind::LocalDate => {
883                                    write!(f, "{:04}-{:02}-{:02}", year, month, day)?;
884                                }
885                                DynDateTimeKind::LocalTime => {
886                                    if nanos > 0 {
887                                        write!(
888                                            f,
889                                            "{:02}:{:02}:{:02}.{:09}",
890                                            hour, minute, second, nanos
891                                        )?;
892                                    } else {
893                                        write!(f, "{:02}:{:02}:{:02}", hour, minute, second)?;
894                                    }
895                                }
896                            }
897                        }
898                    }
899                    DynValueKind::QName => {
900                        // QName formatting is not yet supported via vtable
901                        write!(f, "<qname>")?;
902                    }
903                    DynValueKind::Uuid => {
904                        // UUID formatting is not yet supported via vtable
905                        write!(f, "<uuid>")?;
906                    }
907                }
908            }
909
910            (d, t) => write!(f, "unsupported peek variant: {value:?} ({d:?}, {t:?})")?,
911        }
912
913        visited.remove(&value.id());
914        Ok(())
915    }
916
917    /// Format a value through its proxy type representation.
918    ///
919    /// This allocates memory for the proxy type, converts the value to its proxy
920    /// representation, formats the proxy, then cleans up.
921    #[allow(clippy::too_many_arguments)]
922    fn format_via_proxy(
923        &self,
924        value: Peek<'_, '_>,
925        proxy_def: &'static facet_core::ProxyDef,
926        f: &mut dyn Write,
927        visited: &mut BTreeMap<ValueId, usize>,
928        format_depth: usize,
929        type_depth: usize,
930        short: bool,
931    ) -> fmt::Result {
932        let proxy_shape = proxy_def.shape;
933        let proxy_layout = match proxy_shape.layout.sized_layout() {
934            Ok(layout) => layout,
935            Err(_) => {
936                return write!(f, "/* proxy type must be sized for formatting */");
937            }
938        };
939
940        // Allocate memory for the proxy value
941        let proxy_mem = unsafe { alloc::alloc::alloc(proxy_layout) };
942        if proxy_mem.is_null() {
943            return write!(f, "/* failed to allocate proxy memory */");
944        }
945
946        // Convert target → proxy
947        let proxy_uninit = PtrUninit::new(proxy_mem);
948        let convert_result = unsafe { (proxy_def.convert_out)(value.data(), proxy_uninit) };
949
950        let proxy_ptr = match convert_result {
951            Ok(ptr) => ptr,
952            Err(msg) => {
953                unsafe { alloc::alloc::dealloc(proxy_mem, proxy_layout) };
954                return write!(f, "/* proxy conversion failed: {msg} */");
955            }
956        };
957
958        // Create a Peek to the proxy value and format it
959        let proxy_peek = unsafe { Peek::unchecked_new(proxy_ptr.as_const(), proxy_shape) };
960        let result =
961            self.format_peek_internal_(proxy_peek, f, visited, format_depth, type_depth, short);
962
963        // Clean up: drop the proxy value and deallocate
964        unsafe {
965            let _ = proxy_shape.call_drop_in_place(proxy_ptr);
966            alloc::alloc::dealloc(proxy_mem, proxy_layout);
967        }
968
969        result
970    }
971
972    /// Format a value through its proxy type representation (unified version for FormatOutput).
973    ///
974    /// This allocates memory for the proxy type, converts the value to its proxy
975    /// representation, formats the proxy, then cleans up.
976    #[allow(clippy::too_many_arguments)]
977    fn format_via_proxy_unified<O: FormatOutput>(
978        &self,
979        value: Peek<'_, '_>,
980        proxy_def: &'static facet_core::ProxyDef,
981        out: &mut O,
982        visited: &mut BTreeMap<ValueId, usize>,
983        format_depth: usize,
984        type_depth: usize,
985        short: bool,
986        current_path: Path,
987    ) -> fmt::Result {
988        let proxy_shape = proxy_def.shape;
989        let proxy_layout = match proxy_shape.layout.sized_layout() {
990            Ok(layout) => layout,
991            Err(_) => {
992                return write!(out, "/* proxy type must be sized for formatting */");
993            }
994        };
995
996        // Allocate memory for the proxy value
997        let proxy_mem = unsafe { alloc::alloc::alloc(proxy_layout) };
998        if proxy_mem.is_null() {
999            return write!(out, "/* failed to allocate proxy memory */");
1000        }
1001
1002        // Convert target → proxy
1003        let proxy_uninit = PtrUninit::new(proxy_mem);
1004        let convert_result = unsafe { (proxy_def.convert_out)(value.data(), proxy_uninit) };
1005
1006        let proxy_ptr = match convert_result {
1007            Ok(ptr) => ptr,
1008            Err(msg) => {
1009                unsafe { alloc::alloc::dealloc(proxy_mem, proxy_layout) };
1010                return write!(out, "/* proxy conversion failed: {msg} */");
1011            }
1012        };
1013
1014        // Create a Peek to the proxy value and format it
1015        let proxy_peek = unsafe { Peek::unchecked_new(proxy_ptr.as_const(), proxy_shape) };
1016        let result = self.format_unified(
1017            proxy_peek,
1018            out,
1019            visited,
1020            format_depth,
1021            type_depth,
1022            short,
1023            current_path,
1024        );
1025
1026        // Clean up: drop the proxy value and deallocate
1027        unsafe {
1028            let _ = proxy_shape.call_drop_in_place(proxy_ptr);
1029            alloc::alloc::dealloc(proxy_mem, proxy_layout);
1030        }
1031
1032        result
1033    }
1034
1035    #[allow(clippy::too_many_arguments)]
1036    fn format_tuple_fields<'mem, 'facet>(
1037        &self,
1038        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
1039        f: &mut dyn Write,
1040        visited: &mut BTreeMap<ValueId, usize>,
1041        format_depth: usize,
1042        type_depth: usize,
1043        fields: &[Field],
1044        short: bool,
1045        force_trailing_comma: bool,
1046    ) -> fmt::Result {
1047        self.write_punctuation(f, "(")?;
1048        if let [field] = fields
1049            && field.doc.is_empty()
1050        {
1051            let field_value = peek_field(0);
1052            if let Some(proxy_def) = field.proxy() {
1053                self.format_via_proxy(
1054                    field_value,
1055                    proxy_def,
1056                    f,
1057                    visited,
1058                    format_depth,
1059                    type_depth,
1060                    short,
1061                )?;
1062            } else {
1063                self.format_peek_internal_(
1064                    field_value,
1065                    f,
1066                    visited,
1067                    format_depth,
1068                    type_depth,
1069                    short,
1070                )?;
1071            }
1072
1073            if force_trailing_comma {
1074                self.write_punctuation(f, ",")?;
1075            }
1076        } else if !fields.is_empty() {
1077            for idx in 0..fields.len() {
1078                if !short {
1079                    writeln!(f)?;
1080                    self.indent(f, format_depth + 1)?;
1081
1082                    if self.show_doc_comments {
1083                        for &line in fields[idx].doc {
1084                            self.write_comment(f, &format!("///{line}"))?;
1085                            writeln!(f)?;
1086                            self.indent(f, format_depth + 1)?;
1087                        }
1088                    }
1089                }
1090
1091                if fields[idx].is_sensitive() {
1092                    self.write_redacted(f, "[REDACTED]")?;
1093                } else if let Some(proxy_def) = fields[idx].proxy() {
1094                    // Field-level proxy: format through the proxy type
1095                    self.format_via_proxy(
1096                        peek_field(idx),
1097                        proxy_def,
1098                        f,
1099                        visited,
1100                        format_depth + 1,
1101                        type_depth + 1,
1102                        short,
1103                    )?;
1104                } else {
1105                    self.format_peek_internal_(
1106                        peek_field(idx),
1107                        f,
1108                        visited,
1109                        format_depth + 1,
1110                        type_depth + 1,
1111                        short,
1112                    )?;
1113                }
1114
1115                if !short || idx + 1 < fields.len() {
1116                    self.write_punctuation(f, ",")?;
1117                } else {
1118                    write!(f, " ")?;
1119                }
1120            }
1121            if !short {
1122                writeln!(f)?;
1123                self.indent(f, format_depth)?;
1124            }
1125        }
1126        self.write_punctuation(f, ")")?;
1127        Ok(())
1128    }
1129
1130    #[allow(clippy::too_many_arguments)]
1131    fn format_struct_fields<'mem, 'facet>(
1132        &self,
1133        peek_field: &dyn Fn(usize) -> Peek<'mem, 'facet>,
1134        f: &mut dyn Write,
1135        visited: &mut BTreeMap<ValueId, usize>,
1136        format_depth: usize,
1137        type_depth: usize,
1138        fields: &[Field],
1139        short: bool,
1140    ) -> fmt::Result {
1141        // First, determine which fields will be printed (not skipped)
1142        let visible_indices: Vec<usize> = (0..fields.len())
1143            .filter(|&idx| {
1144                let field = &fields[idx];
1145                // SAFETY: peek_field returns a valid Peek with valid data pointer
1146                let field_ptr = peek_field(idx).data();
1147                !unsafe { field.should_skip_serializing(field_ptr) }
1148            })
1149            .collect();
1150
1151        self.write_punctuation(f, " {")?;
1152        if !visible_indices.is_empty() {
1153            for (i, &idx) in visible_indices.iter().enumerate() {
1154                let is_last = i + 1 == visible_indices.len();
1155
1156                if !short {
1157                    writeln!(f)?;
1158                    self.indent(f, format_depth + 1)?;
1159                }
1160
1161                if self.show_doc_comments {
1162                    for &line in fields[idx].doc {
1163                        self.write_comment(f, &format!("///{line}"))?;
1164                        writeln!(f)?;
1165                        self.indent(f, format_depth + 1)?;
1166                    }
1167                }
1168
1169                self.write_field_name(f, fields[idx].name)?;
1170                self.write_punctuation(f, ": ")?;
1171                if fields[idx].is_sensitive() {
1172                    self.write_redacted(f, "[REDACTED]")?;
1173                } else if let Some(proxy_def) = fields[idx].proxy() {
1174                    // Field-level proxy: format through the proxy type
1175                    self.format_via_proxy(
1176                        peek_field(idx),
1177                        proxy_def,
1178                        f,
1179                        visited,
1180                        format_depth + 1,
1181                        type_depth + 1,
1182                        short,
1183                    )?;
1184                } else {
1185                    self.format_peek_internal_(
1186                        peek_field(idx),
1187                        f,
1188                        visited,
1189                        format_depth + 1,
1190                        type_depth + 1,
1191                        short,
1192                    )?;
1193                }
1194
1195                if !short || !is_last {
1196                    self.write_punctuation(f, ",")?;
1197                } else {
1198                    write!(f, " ")?;
1199                }
1200            }
1201            if !short {
1202                writeln!(f)?;
1203                self.indent(f, format_depth)?;
1204            }
1205        }
1206        self.write_punctuation(f, "}")?;
1207        Ok(())
1208    }
1209
1210    fn indent(&self, f: &mut dyn Write, indent: usize) -> fmt::Result {
1211        if self.indent_size == usize::MAX {
1212            write!(f, "{:\t<width$}", "", width = indent)
1213        } else {
1214            write!(f, "{: <width$}", "", width = indent * self.indent_size)
1215        }
1216    }
1217
1218    /// Internal method to format a Peek value
1219    pub(crate) fn format_peek_internal(
1220        &self,
1221        value: Peek<'_, '_>,
1222        f: &mut dyn Write,
1223        visited: &mut BTreeMap<ValueId, usize>,
1224    ) -> fmt::Result {
1225        self.format_peek_internal_(value, f, visited, 0, 0, false)
1226    }
1227
1228    /// Format a scalar value
1229    fn format_scalar(&self, value: Peek, f: &mut dyn Write) -> fmt::Result {
1230        // Generate a color for this shape
1231        let mut hasher = DefaultHasher::new();
1232        value.shape().id.hash(&mut hasher);
1233        let hash = hasher.finish();
1234        let color = self.color_generator.generate_color(hash);
1235
1236        // Display the value
1237        struct DisplayWrapper<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
1238
1239        impl fmt::Display for DisplayWrapper<'_, '_> {
1240            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1241                if self.0.shape().is_display() {
1242                    write!(f, "{}", self.0)?;
1243                } else if self.0.shape().is_debug() {
1244                    write!(f, "{:?}", self.0)?;
1245                } else {
1246                    write!(f, "{}", self.0.shape())?;
1247                    write!(f, "(…)")?;
1248                }
1249                Ok(())
1250            }
1251        }
1252
1253        // Apply color if needed and display
1254        if self.use_colors() {
1255            let rgb = Rgb(color.r, color.g, color.b);
1256            write!(f, "{}", DisplayWrapper(&value).color(rgb))?;
1257        } else {
1258            write!(f, "{}", DisplayWrapper(&value))?;
1259        }
1260
1261        Ok(())
1262    }
1263
1264    /// Write a keyword (null, true, false) with coloring
1265    fn write_keyword(&self, f: &mut dyn Write, keyword: &str) -> fmt::Result {
1266        if self.use_colors() {
1267            write!(f, "{}", keyword.color(tokyo_night::KEYWORD))
1268        } else {
1269            write!(f, "{keyword}")
1270        }
1271    }
1272
1273    /// Format a number for dynamic values
1274    fn format_number(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1275        if self.use_colors() {
1276            write!(f, "{}", s.color(tokyo_night::NUMBER))
1277        } else {
1278            write!(f, "{s}")
1279        }
1280    }
1281
1282    /// Format a &str or String value with optional truncation and raw string handling
1283    fn format_str_value(&self, f: &mut dyn Write, value: &str) -> fmt::Result {
1284        // Check if truncation is needed
1285        if let Some(max) = self.max_content_len
1286            && value.len() > max
1287        {
1288            return self.format_truncated_str(f, value, max);
1289        }
1290
1291        // Normal formatting with raw string handling for quotes
1292        let mut hashes = 0usize;
1293        let mut rest = value;
1294        while let Some(idx) = rest.find('"') {
1295            rest = &rest[idx + 1..];
1296            let before = rest.len();
1297            rest = rest.trim_start_matches('#');
1298            let after = rest.len();
1299            let count = before - after;
1300            hashes = Ord::max(hashes, 1 + count);
1301        }
1302
1303        let pad = "";
1304        let width = hashes.saturating_sub(1);
1305        if hashes > 0 {
1306            write!(f, "r{pad:#<width$}")?;
1307        }
1308        write!(f, "\"")?;
1309        if self.use_colors() {
1310            write!(f, "{}", value.color(tokyo_night::STRING))?;
1311        } else {
1312            write!(f, "{value}")?;
1313        }
1314        write!(f, "\"")?;
1315        if hashes > 0 {
1316            write!(f, "{pad:#<width$}")?;
1317        }
1318        Ok(())
1319    }
1320
1321    /// Format a truncated string showing beginning...end
1322    fn format_truncated_str(&self, f: &mut dyn Write, s: &str, max: usize) -> fmt::Result {
1323        let half = max / 2;
1324
1325        // Find char boundary for start portion
1326        let start_end = s
1327            .char_indices()
1328            .take_while(|(i, _)| *i < half)
1329            .last()
1330            .map(|(i, c)| i + c.len_utf8())
1331            .unwrap_or(0);
1332
1333        // Find char boundary for end portion
1334        let end_start = s
1335            .char_indices()
1336            .rev()
1337            .take_while(|(i, _)| s.len() - *i <= half)
1338            .last()
1339            .map(|(i, _)| i)
1340            .unwrap_or(s.len());
1341
1342        let omitted = s[start_end..end_start].chars().count();
1343        let start_part = &s[..start_end];
1344        let end_part = &s[end_start..];
1345
1346        if self.use_colors() {
1347            write!(
1348                f,
1349                "\"{}\"...({omitted} chars)...\"{}\"",
1350                start_part.color(tokyo_night::STRING),
1351                end_part.color(tokyo_night::STRING)
1352            )
1353        } else {
1354            write!(f, "\"{start_part}\"...({omitted} chars)...\"{end_part}\"")
1355        }
1356    }
1357
1358    /// Format a string for dynamic values (uses debug escaping for special chars)
1359    fn format_string(&self, f: &mut dyn Write, s: &str) -> fmt::Result {
1360        if let Some(max) = self.max_content_len
1361            && s.len() > max
1362        {
1363            return self.format_truncated_str(f, s, max);
1364        }
1365
1366        if self.use_colors() {
1367            write!(f, "\"{}\"", s.color(tokyo_night::STRING))
1368        } else {
1369            write!(f, "{s:?}")
1370        }
1371    }
1372
1373    /// Format bytes for dynamic values
1374    fn format_bytes(&self, f: &mut dyn Write, bytes: &[u8]) -> fmt::Result {
1375        write!(f, "b\"")?;
1376
1377        match self.max_content_len {
1378            Some(max) if bytes.len() > max => {
1379                // Show beginning ... end
1380                let half = max / 2;
1381                let start = half;
1382                let end = half;
1383
1384                for byte in &bytes[..start] {
1385                    write!(f, "\\x{byte:02x}")?;
1386                }
1387                let omitted = bytes.len() - start - end;
1388                write!(f, "\"...({omitted} bytes)...b\"")?;
1389                for byte in &bytes[bytes.len() - end..] {
1390                    write!(f, "\\x{byte:02x}")?;
1391                }
1392            }
1393            _ => {
1394                for byte in bytes {
1395                    write!(f, "\\x{byte:02x}")?;
1396                }
1397            }
1398        }
1399
1400        write!(f, "\"")
1401    }
1402
1403    /// Write styled type name to formatter
1404    fn write_type_name(&self, f: &mut dyn Write, peek: &Peek) -> fmt::Result {
1405        struct TypeNameWriter<'mem, 'facet>(&'mem Peek<'mem, 'facet>);
1406
1407        impl core::fmt::Display for TypeNameWriter<'_, '_> {
1408            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1409                self.0.type_name(f, TypeNameOpts::infinite())
1410            }
1411        }
1412        let type_name = TypeNameWriter(peek);
1413
1414        if self.use_colors() {
1415            write!(f, "{}", type_name.color(tokyo_night::TYPE_NAME).bold())
1416        } else {
1417            write!(f, "{type_name}")
1418        }
1419    }
1420
1421    /// Style a type name and return it as a string
1422    #[allow(dead_code)]
1423    fn style_type_name(&self, peek: &Peek) -> String {
1424        let mut result = String::new();
1425        self.write_type_name(&mut result, peek).unwrap();
1426        result
1427    }
1428
1429    /// Write styled field name to formatter
1430    fn write_field_name(&self, f: &mut dyn Write, name: &str) -> fmt::Result {
1431        if self.use_colors() {
1432            write!(f, "{}", name.color(tokyo_night::FIELD_NAME))
1433        } else {
1434            write!(f, "{name}")
1435        }
1436    }
1437
1438    /// Write styled punctuation to formatter
1439    fn write_punctuation(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1440        if self.use_colors() {
1441            write!(f, "{}", text.dimmed())
1442        } else {
1443            write!(f, "{text}")
1444        }
1445    }
1446
1447    /// Write styled comment to formatter
1448    fn write_comment(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1449        if self.use_colors() {
1450            write!(f, "{}", text.color(tokyo_night::MUTED))
1451        } else {
1452            write!(f, "{text}")
1453        }
1454    }
1455
1456    /// Write styled redacted value to formatter
1457    fn write_redacted(&self, f: &mut dyn Write, text: &str) -> fmt::Result {
1458        if self.use_colors() {
1459            write!(f, "{}", text.color(tokyo_night::ERROR).bold())
1460        } else {
1461            write!(f, "{text}")
1462        }
1463    }
1464
1465    /// Style a redacted value and return it as a string
1466    #[allow(dead_code)]
1467    fn style_redacted(&self, text: &str) -> String {
1468        let mut result = String::new();
1469        self.write_redacted(&mut result, text).unwrap();
1470        result
1471    }
1472
1473    /// Format a value with span tracking for each path.
1474    ///
1475    /// Returns a `FormattedValue` containing the plain text output and a map
1476    /// from paths to their byte spans in the output.
1477    ///
1478    /// This is useful for creating rich diagnostics that can highlight specific
1479    /// parts of a pretty-printed value.
1480    pub fn format_peek_with_spans(&self, value: Peek<'_, '_>) -> FormattedValue {
1481        let mut output = SpanTrackingOutput::new();
1482        let printer = Self {
1483            colors: ColorMode::Never, // Always disable colors for span tracking
1484            indent_size: self.indent_size,
1485            max_depth: self.max_depth,
1486            color_generator: self.color_generator.clone(),
1487            list_u8_as_bytes: self.list_u8_as_bytes,
1488            minimal_option_names: self.minimal_option_names,
1489            show_doc_comments: self.show_doc_comments,
1490            max_content_len: self.max_content_len,
1491        };
1492        printer
1493            .format_unified(
1494                value,
1495                &mut output,
1496                &mut BTreeMap::new(),
1497                0,
1498                0,
1499                false,
1500                vec![],
1501            )
1502            .expect("Formatting failed");
1503
1504        output.into_formatted_value()
1505    }
1506
1507    /// Unified formatting implementation that works with any FormatOutput.
1508    ///
1509    /// This is the core implementation - both `format_peek` and `format_peek_with_spans`
1510    /// use this internally with different output types.
1511    #[allow(clippy::too_many_arguments)]
1512    fn format_unified<O: FormatOutput>(
1513        &self,
1514        value: Peek<'_, '_>,
1515        out: &mut O,
1516        visited: &mut BTreeMap<ValueId, usize>,
1517        format_depth: usize,
1518        type_depth: usize,
1519        short: bool,
1520        current_path: Path,
1521    ) -> fmt::Result {
1522        let mut value = value;
1523        while let Ok(ptr) = value.into_pointer()
1524            && let Some(pointee) = ptr.borrow_inner()
1525        {
1526            value = pointee;
1527        }
1528
1529        // Unwrap transparent wrappers (e.g., newtype wrappers like IntAsString(String))
1530        // This matches serialization behavior where we serialize the inner value directly
1531        let value = value.innermost_peek();
1532        let shape = value.shape();
1533
1534        // Record the start of this value
1535        let value_start = out.position();
1536
1537        if let Some(prev_type_depth) = visited.insert(value.id(), type_depth) {
1538            write!(out, "{} {{ ", shape.type_identifier)?;
1539            write!(
1540                out,
1541                "/* cycle detected at {} (first seen at type_depth {}) */",
1542                value.id(),
1543                prev_type_depth,
1544            )?;
1545            visited.remove(&value.id());
1546            let value_end = out.position();
1547            out.record_span(current_path, (value_start, value_end));
1548            return Ok(());
1549        }
1550
1551        // Handle proxy types by converting to the proxy representation and formatting that
1552        if let Some(proxy_def) = shape.proxy {
1553            let result = self.format_via_proxy_unified(
1554                value,
1555                proxy_def,
1556                out,
1557                visited,
1558                format_depth,
1559                type_depth,
1560                short,
1561                current_path.clone(),
1562            );
1563
1564            visited.remove(&value.id());
1565
1566            // Record span for this value
1567            let value_end = out.position();
1568            out.record_span(current_path, (value_start, value_end));
1569
1570            return result;
1571        }
1572
1573        match (shape.def, shape.ty) {
1574            (_, Type::Primitive(PrimitiveType::Textual(TextualType::Str))) => {
1575                let s = value.get::<str>().unwrap();
1576                write!(out, "\"{}\"", s)?;
1577            }
1578            (Def::Scalar, _) if value.shape().id == <alloc::string::String as Facet>::SHAPE.id => {
1579                let s = value.get::<alloc::string::String>().unwrap();
1580                write!(out, "\"{}\"", s)?;
1581            }
1582            (Def::Scalar, _) => {
1583                self.format_scalar_to_output(value, out)?;
1584            }
1585            (Def::Option(_), _) => {
1586                let option = value.into_option().unwrap();
1587                if let Some(inner) = option.value() {
1588                    write!(out, "Some(")?;
1589                    self.format_unified(
1590                        inner,
1591                        out,
1592                        visited,
1593                        format_depth,
1594                        type_depth + 1,
1595                        short,
1596                        current_path.clone(),
1597                    )?;
1598                    write!(out, ")")?;
1599                } else {
1600                    write!(out, "None")?;
1601                }
1602            }
1603            (
1604                _,
1605                Type::User(UserType::Struct(
1606                    ty @ StructType {
1607                        kind: StructKind::Struct | StructKind::Unit,
1608                        ..
1609                    },
1610                )),
1611            ) => {
1612                write!(out, "{}", shape.type_identifier)?;
1613                if matches!(ty.kind, StructKind::Struct) {
1614                    let struct_peek = value.into_struct().unwrap();
1615                    write!(out, " {{")?;
1616                    for (i, field) in ty.fields.iter().enumerate() {
1617                        if !short {
1618                            writeln!(out)?;
1619                            self.indent_to_output(out, format_depth + 1)?;
1620                        }
1621                        // Record field name span
1622                        let field_name_start = out.position();
1623                        write!(out, "{}", field.name)?;
1624                        let field_name_end = out.position();
1625                        write!(out, ": ")?;
1626
1627                        // Build path for this field
1628                        let mut field_path = current_path.clone();
1629                        field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1630
1631                        // Record field value span
1632                        let field_value_start = out.position();
1633                        if let Ok(field_value) = struct_peek.field(i) {
1634                            // Check for field-level proxy
1635                            if let Some(proxy_def) = field.proxy() {
1636                                self.format_via_proxy_unified(
1637                                    field_value,
1638                                    proxy_def,
1639                                    out,
1640                                    visited,
1641                                    format_depth + 1,
1642                                    type_depth + 1,
1643                                    short,
1644                                    field_path.clone(),
1645                                )?;
1646                            } else {
1647                                self.format_unified(
1648                                    field_value,
1649                                    out,
1650                                    visited,
1651                                    format_depth + 1,
1652                                    type_depth + 1,
1653                                    short,
1654                                    field_path.clone(),
1655                                )?;
1656                            }
1657                        }
1658                        let field_value_end = out.position();
1659
1660                        // Record span for this field
1661                        out.record_field_span(
1662                            field_path,
1663                            (field_name_start, field_name_end),
1664                            (field_value_start, field_value_end),
1665                        );
1666
1667                        if !short || i + 1 < ty.fields.len() {
1668                            write!(out, ",")?;
1669                        }
1670                    }
1671                    if !short {
1672                        writeln!(out)?;
1673                        self.indent_to_output(out, format_depth)?;
1674                    }
1675                    write!(out, "}}")?;
1676                }
1677            }
1678            (
1679                _,
1680                Type::User(UserType::Struct(
1681                    ty @ StructType {
1682                        kind: StructKind::Tuple | StructKind::TupleStruct,
1683                        ..
1684                    },
1685                )),
1686            ) => {
1687                write!(out, "{}", shape.type_identifier)?;
1688                if matches!(ty.kind, StructKind::Tuple) {
1689                    write!(out, " ")?;
1690                }
1691                let struct_peek = value.into_struct().unwrap();
1692                write!(out, "(")?;
1693                for (i, field) in ty.fields.iter().enumerate() {
1694                    if i > 0 {
1695                        write!(out, ", ")?;
1696                    }
1697                    let mut elem_path = current_path.clone();
1698                    elem_path.push(PathSegment::Index(i));
1699
1700                    let elem_start = out.position();
1701                    if let Ok(field_value) = struct_peek.field(i) {
1702                        // Check for field-level proxy
1703                        if let Some(proxy_def) = field.proxy() {
1704                            self.format_via_proxy_unified(
1705                                field_value,
1706                                proxy_def,
1707                                out,
1708                                visited,
1709                                format_depth + 1,
1710                                type_depth + 1,
1711                                short,
1712                                elem_path.clone(),
1713                            )?;
1714                        } else {
1715                            self.format_unified(
1716                                field_value,
1717                                out,
1718                                visited,
1719                                format_depth + 1,
1720                                type_depth + 1,
1721                                short,
1722                                elem_path.clone(),
1723                            )?;
1724                        }
1725                    }
1726                    let elem_end = out.position();
1727                    out.record_span(elem_path, (elem_start, elem_end));
1728                }
1729                write!(out, ")")?;
1730            }
1731            (_, Type::User(UserType::Enum(_))) => {
1732                let enum_peek = value.into_enum().unwrap();
1733                match enum_peek.active_variant() {
1734                    Err(_) => {
1735                        write!(
1736                            out,
1737                            "{} {{ /* cannot determine variant */ }}",
1738                            shape.type_identifier
1739                        )?;
1740                    }
1741                    Ok(variant) => {
1742                        write!(out, "{}::{}", shape.type_identifier, variant.name)?;
1743
1744                        match variant.data.kind {
1745                            StructKind::Unit => {}
1746                            StructKind::Struct => {
1747                                write!(out, " {{")?;
1748                                for (i, field) in variant.data.fields.iter().enumerate() {
1749                                    if !short {
1750                                        writeln!(out)?;
1751                                        self.indent_to_output(out, format_depth + 1)?;
1752                                    }
1753                                    let field_name_start = out.position();
1754                                    write!(out, "{}", field.name)?;
1755                                    let field_name_end = out.position();
1756                                    write!(out, ": ")?;
1757
1758                                    let mut field_path = current_path.clone();
1759                                    field_path
1760                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1761                                    field_path.push(PathSegment::Field(Cow::Borrowed(field.name)));
1762
1763                                    let field_value_start = out.position();
1764                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1765                                        // Check for field-level proxy
1766                                        if let Some(proxy_def) = field.proxy() {
1767                                            self.format_via_proxy_unified(
1768                                                field_value,
1769                                                proxy_def,
1770                                                out,
1771                                                visited,
1772                                                format_depth + 1,
1773                                                type_depth + 1,
1774                                                short,
1775                                                field_path.clone(),
1776                                            )?;
1777                                        } else {
1778                                            self.format_unified(
1779                                                field_value,
1780                                                out,
1781                                                visited,
1782                                                format_depth + 1,
1783                                                type_depth + 1,
1784                                                short,
1785                                                field_path.clone(),
1786                                            )?;
1787                                        }
1788                                    }
1789                                    let field_value_end = out.position();
1790
1791                                    out.record_field_span(
1792                                        field_path,
1793                                        (field_name_start, field_name_end),
1794                                        (field_value_start, field_value_end),
1795                                    );
1796
1797                                    if !short || i + 1 < variant.data.fields.len() {
1798                                        write!(out, ",")?;
1799                                    }
1800                                }
1801                                if !short {
1802                                    writeln!(out)?;
1803                                    self.indent_to_output(out, format_depth)?;
1804                                }
1805                                write!(out, "}}")?;
1806                            }
1807                            _ => {
1808                                write!(out, "(")?;
1809                                for (i, field) in variant.data.fields.iter().enumerate() {
1810                                    if i > 0 {
1811                                        write!(out, ", ")?;
1812                                    }
1813                                    let mut elem_path = current_path.clone();
1814                                    elem_path
1815                                        .push(PathSegment::Variant(Cow::Borrowed(variant.name)));
1816                                    elem_path.push(PathSegment::Index(i));
1817
1818                                    let elem_start = out.position();
1819                                    if let Ok(Some(field_value)) = enum_peek.field(i) {
1820                                        // Check for field-level proxy
1821                                        if let Some(proxy_def) = field.proxy() {
1822                                            self.format_via_proxy_unified(
1823                                                field_value,
1824                                                proxy_def,
1825                                                out,
1826                                                visited,
1827                                                format_depth + 1,
1828                                                type_depth + 1,
1829                                                short,
1830                                                elem_path.clone(),
1831                                            )?;
1832                                        } else {
1833                                            self.format_unified(
1834                                                field_value,
1835                                                out,
1836                                                visited,
1837                                                format_depth + 1,
1838                                                type_depth + 1,
1839                                                short,
1840                                                elem_path.clone(),
1841                                            )?;
1842                                        }
1843                                    }
1844                                    let elem_end = out.position();
1845                                    out.record_span(elem_path, (elem_start, elem_end));
1846                                }
1847                                write!(out, ")")?;
1848                            }
1849                        }
1850                    }
1851                }
1852            }
1853            _ if value.into_list_like().is_ok() => {
1854                let list = value.into_list_like().unwrap();
1855
1856                // Check if elements are simple scalars - render inline if so
1857                let elem_shape = list.def().t();
1858                let is_simple = Self::shape_chunkiness(elem_shape) <= 1;
1859
1860                write!(out, "[")?;
1861                let len = list.len();
1862                for (i, item) in list.iter().enumerate() {
1863                    if !short && !is_simple {
1864                        writeln!(out)?;
1865                        self.indent_to_output(out, format_depth + 1)?;
1866                    } else if i > 0 {
1867                        write!(out, " ")?;
1868                    }
1869                    let mut elem_path = current_path.clone();
1870                    elem_path.push(PathSegment::Index(i));
1871
1872                    let elem_start = out.position();
1873                    self.format_unified(
1874                        item,
1875                        out,
1876                        visited,
1877                        format_depth + 1,
1878                        type_depth + 1,
1879                        short || is_simple,
1880                        elem_path.clone(),
1881                    )?;
1882                    let elem_end = out.position();
1883                    out.record_span(elem_path, (elem_start, elem_end));
1884
1885                    if (!short && !is_simple) || i + 1 < len {
1886                        write!(out, ",")?;
1887                    }
1888                }
1889                if !short && !is_simple {
1890                    writeln!(out)?;
1891                    self.indent_to_output(out, format_depth)?;
1892                }
1893                write!(out, "]")?;
1894            }
1895            _ if value.into_map().is_ok() => {
1896                let map = value.into_map().unwrap();
1897                write!(out, "{{")?;
1898                for (i, (key, val)) in map.iter().enumerate() {
1899                    if !short {
1900                        writeln!(out)?;
1901                        self.indent_to_output(out, format_depth + 1)?;
1902                    }
1903                    // Format key
1904                    let key_start = out.position();
1905                    self.format_unified(
1906                        key,
1907                        out,
1908                        visited,
1909                        format_depth + 1,
1910                        type_depth + 1,
1911                        true, // short for keys
1912                        vec![],
1913                    )?;
1914                    let key_end = out.position();
1915
1916                    write!(out, ": ")?;
1917
1918                    // Build path for this entry (use key's string representation)
1919                    let key_str = self.format_peek(key);
1920                    let mut entry_path = current_path.clone();
1921                    entry_path.push(PathSegment::Key(Cow::Owned(key_str)));
1922
1923                    let val_start = out.position();
1924                    self.format_unified(
1925                        val,
1926                        out,
1927                        visited,
1928                        format_depth + 1,
1929                        type_depth + 1,
1930                        short,
1931                        entry_path.clone(),
1932                    )?;
1933                    let val_end = out.position();
1934
1935                    out.record_field_span(entry_path, (key_start, key_end), (val_start, val_end));
1936
1937                    if !short || i + 1 < map.len() {
1938                        write!(out, ",")?;
1939                    }
1940                }
1941                if !short && !map.is_empty() {
1942                    writeln!(out)?;
1943                    self.indent_to_output(out, format_depth)?;
1944                }
1945                write!(out, "}}")?;
1946            }
1947            _ => {
1948                // Fallback: just write the type name
1949                write!(out, "{} {{ ... }}", shape.type_identifier)?;
1950            }
1951        }
1952
1953        visited.remove(&value.id());
1954
1955        // Record span for this value
1956        let value_end = out.position();
1957        out.record_span(current_path, (value_start, value_end));
1958
1959        Ok(())
1960    }
1961
1962    fn format_scalar_to_output(&self, value: Peek<'_, '_>, out: &mut impl Write) -> fmt::Result {
1963        // Use Display or Debug trait to format scalar values
1964        if value.shape().is_display() {
1965            write!(out, "{}", value)
1966        } else if value.shape().is_debug() {
1967            write!(out, "{:?}", value)
1968        } else {
1969            write!(out, "{}(…)", value.shape())
1970        }
1971    }
1972
1973    fn indent_to_output(&self, out: &mut impl Write, depth: usize) -> fmt::Result {
1974        for _ in 0..depth {
1975            for _ in 0..self.indent_size {
1976                out.write_char(' ')?;
1977            }
1978        }
1979        Ok(())
1980    }
1981}
1982
1983/// Color mode for the pretty printer.
1984#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
1985pub enum ColorMode {
1986    /// Automtically detect whether colors are desired through the `NO_COLOR` environment variable.
1987    Auto,
1988    /// Always enable colors.
1989    Always,
1990    /// Never enable colors.
1991    Never,
1992}
1993
1994impl ColorMode {
1995    /// Convert the color mode to an option of a boolean.
1996    pub fn enabled(&self) -> bool {
1997        static NO_COLOR: LazyLock<bool> = LazyLock::new(|| std::env::var_os("NO_COLOR").is_some());
1998        match self {
1999            ColorMode::Auto => !*NO_COLOR,
2000            ColorMode::Always => true,
2001            ColorMode::Never => false,
2002        }
2003    }
2004}
2005
2006impl From<bool> for ColorMode {
2007    fn from(value: bool) -> Self {
2008        if value {
2009            ColorMode::Always
2010        } else {
2011            ColorMode::Never
2012        }
2013    }
2014}
2015
2016impl From<ColorMode> for Option<bool> {
2017    fn from(value: ColorMode) -> Self {
2018        match value {
2019            ColorMode::Auto => None,
2020            ColorMode::Always => Some(true),
2021            ColorMode::Never => Some(false),
2022        }
2023    }
2024}
2025
2026/// Result of formatting a value with span tracking
2027#[derive(Debug)]
2028pub struct FormattedValue {
2029    /// The formatted text (plain text, no ANSI colors)
2030    pub text: String,
2031    /// Map from paths to their byte spans in `text`
2032    pub spans: BTreeMap<Path, FieldSpan>,
2033}
2034
2035/// Trait for output destinations that may optionally track spans.
2036///
2037/// This allows a single formatting implementation to work with both
2038/// simple string output and span-tracking output.
2039trait FormatOutput: Write {
2040    /// Get the current byte position in the output (for span tracking)
2041    fn position(&self) -> usize;
2042
2043    /// Record a span for a path (value only, key=value)
2044    fn record_span(&mut self, _path: Path, _span: Span) {}
2045
2046    /// Record a span with separate key and value spans
2047    fn record_field_span(&mut self, _path: Path, _key_span: Span, _value_span: Span) {}
2048}
2049
2050/// A wrapper around any Write that implements FormatOutput but doesn't track spans.
2051/// Position tracking is approximated by counting bytes written.
2052#[allow(dead_code)]
2053struct NonTrackingOutput<W> {
2054    inner: W,
2055    position: usize,
2056}
2057
2058#[allow(dead_code)]
2059impl<W> NonTrackingOutput<W> {
2060    const fn new(inner: W) -> Self {
2061        Self { inner, position: 0 }
2062    }
2063}
2064
2065impl<W: Write> Write for NonTrackingOutput<W> {
2066    fn write_str(&mut self, s: &str) -> fmt::Result {
2067        self.position += s.len();
2068        self.inner.write_str(s)
2069    }
2070}
2071
2072impl<W: Write> FormatOutput for NonTrackingOutput<W> {
2073    fn position(&self) -> usize {
2074        self.position
2075    }
2076    // Uses default no-op implementations for span recording
2077}
2078
2079/// Context for tracking spans during value formatting
2080struct SpanTrackingOutput {
2081    output: String,
2082    spans: BTreeMap<Path, FieldSpan>,
2083}
2084
2085impl SpanTrackingOutput {
2086    const fn new() -> Self {
2087        Self {
2088            output: String::new(),
2089            spans: BTreeMap::new(),
2090        }
2091    }
2092
2093    fn into_formatted_value(self) -> FormattedValue {
2094        FormattedValue {
2095            text: self.output,
2096            spans: self.spans,
2097        }
2098    }
2099}
2100
2101impl Write for SpanTrackingOutput {
2102    fn write_str(&mut self, s: &str) -> fmt::Result {
2103        self.output.push_str(s);
2104        Ok(())
2105    }
2106}
2107
2108impl FormatOutput for SpanTrackingOutput {
2109    fn position(&self) -> usize {
2110        self.output.len()
2111    }
2112
2113    fn record_span(&mut self, path: Path, span: Span) {
2114        self.spans.insert(
2115            path,
2116            FieldSpan {
2117                key: span,
2118                value: span,
2119            },
2120        );
2121    }
2122
2123    fn record_field_span(&mut self, path: Path, key_span: Span, value_span: Span) {
2124        self.spans.insert(
2125            path,
2126            FieldSpan {
2127                key: key_span,
2128                value: value_span,
2129            },
2130        );
2131    }
2132}
2133
2134#[cfg(test)]
2135mod tests {
2136    use super::*;
2137
2138    // Basic tests for the PrettyPrinter
2139    #[test]
2140    fn test_pretty_printer_default() {
2141        let printer = PrettyPrinter::default();
2142        assert_eq!(printer.indent_size, 2);
2143        assert_eq!(printer.max_depth, None);
2144        // use_colors defaults to true unless NO_COLOR is set
2145        // In tests, NO_COLOR=1 is set via nextest config for consistent snapshots
2146        assert_eq!(printer.use_colors(), std::env::var_os("NO_COLOR").is_none());
2147    }
2148
2149    #[test]
2150    fn test_pretty_printer_with_methods() {
2151        let printer = PrettyPrinter::new()
2152            .with_indent_size(4)
2153            .with_max_depth(3)
2154            .with_colors(ColorMode::Never);
2155
2156        assert_eq!(printer.indent_size, 4);
2157        assert_eq!(printer.max_depth, Some(3));
2158        assert!(!printer.use_colors());
2159    }
2160
2161    #[test]
2162    fn test_format_peek_with_spans() {
2163        use crate::PathSegment;
2164        use facet_reflect::Peek;
2165
2166        // Test with a simple tuple - no need for custom struct
2167        let value = ("Alice", 30u32);
2168
2169        let printer = PrettyPrinter::new();
2170        let formatted = printer.format_peek_with_spans(Peek::new(&value));
2171
2172        // Check that we got output
2173        assert!(!formatted.text.is_empty());
2174        assert!(formatted.text.contains("Alice"));
2175        assert!(formatted.text.contains("30"));
2176
2177        // Check that spans were recorded
2178        assert!(!formatted.spans.is_empty());
2179
2180        // Check that the root span exists (empty path)
2181        assert!(formatted.spans.contains_key(&vec![]));
2182
2183        // Check that index spans exist
2184        let idx0_path = vec![PathSegment::Index(0)];
2185        let idx1_path = vec![PathSegment::Index(1)];
2186        assert!(
2187            formatted.spans.contains_key(&idx0_path),
2188            "index 0 span not found"
2189        );
2190        assert!(
2191            formatted.spans.contains_key(&idx1_path),
2192            "index 1 span not found"
2193        );
2194    }
2195
2196    #[test]
2197    fn test_max_content_len_string() {
2198        let printer = PrettyPrinter::new()
2199            .with_colors(ColorMode::Never)
2200            .with_max_content_len(20);
2201
2202        // Short string - no truncation
2203        let short = "hello";
2204        let output = printer.format(&short);
2205        assert_eq!(output, "\"hello\"");
2206
2207        // Long string - should truncate middle
2208        let long = "abcdefghijklmnopqrstuvwxyz0123456789";
2209        let output = printer.format(&long);
2210        assert!(
2211            output.contains("..."),
2212            "should contain ellipsis: {}",
2213            output
2214        );
2215        assert!(output.contains("chars"), "should mention chars: {}", output);
2216        assert!(
2217            output.starts_with("\"abc"),
2218            "should start with beginning: {}",
2219            output
2220        );
2221        assert!(
2222            output.ends_with("89\""),
2223            "should end with ending: {}",
2224            output
2225        );
2226    }
2227
2228    #[test]
2229    fn test_max_content_len_bytes() {
2230        let printer = PrettyPrinter::new()
2231            .with_colors(ColorMode::Never)
2232            .with_max_content_len(10);
2233
2234        // Short bytes - no truncation
2235        let short: Vec<u8> = vec![1, 2, 3];
2236        let output = printer.format(&short);
2237        assert!(
2238            output.contains("01 02 03"),
2239            "should show all bytes: {}",
2240            output
2241        );
2242
2243        // Long bytes - should truncate middle
2244        let long: Vec<u8> = (0..50).collect();
2245        let output = printer.format(&long);
2246        assert!(
2247            output.contains("..."),
2248            "should contain ellipsis: {}",
2249            output
2250        );
2251        assert!(output.contains("bytes"), "should mention bytes: {}", output);
2252    }
2253}