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