1use std::{collections::HashSet, fmt::Write, io::IsTerminal};
4
5use inspect_core::{Children, Kind, Sensitivity, ValueRef};
6
7use crate::{color_choice::ColorChoice, role::StyleRole, theme::Theme};
8
9#[derive(Debug, Clone)]
11pub struct TreeConfig {
12 pub show_types: bool,
14
15 pub indent: String,
17
18 pub use_unicode: bool,
20
21 pub redact_secrets: bool,
23
24 pub show_truncation: bool,
26
27 pub max_items: Option<usize>,
29
30 pub max_depth: Option<usize>,
32
33 pub color_choice: ColorChoice,
35
36 pub theme: Theme,
38}
39
40impl Default for TreeConfig {
41 fn default() -> Self {
42 Self {
43 show_types: true,
44 indent: " ".to_string(),
45 use_unicode: true,
46 redact_secrets: true,
47 show_truncation: true,
48 max_items: None,
49 max_depth: None,
50 color_choice: ColorChoice::Auto,
51 theme: Theme::default(),
52 }
53 }
54}
55
56impl TreeConfig {
57 pub fn compact() -> Self {
59 Self { show_types: false, show_truncation: false, ..Default::default() }
60 }
61
62 pub fn plain() -> Self {
64 Self { color_choice: ColorChoice::Never, ..Default::default() }
65 }
66
67 pub fn colored() -> Self {
69 Self { color_choice: ColorChoice::Always, ..Default::default() }
70 }
71
72 pub fn with_theme(mut self, theme: Theme) -> Self {
74 self.theme = theme;
75 self
76 }
77
78 pub fn with_color_choice(mut self, choice: ColorChoice) -> Self {
80 self.color_choice = choice;
81 self
82 }
83
84 pub fn with_color(mut self, enabled: bool) -> Self {
86 self.color_choice = if enabled { ColorChoice::Always } else { ColorChoice::Never };
87 self
88 }
89
90 pub fn with_max_items(mut self, max: usize) -> Self {
92 self.max_items = Some(max);
93 self
94 }
95
96 pub fn with_max_depth(mut self, depth: usize) -> Self {
98 self.max_depth = Some(depth);
99 self
100 }
101}
102
103struct StyledWriter<'w, W> {
105 writer: &'w mut W,
106 color_enabled: bool,
107}
108
109impl<'w, W: Write> StyledWriter<'w, W> {
110 #[inline(always)]
111 fn write_plain(&mut self, s: &str) -> std::fmt::Result {
112 self.writer.write_str(s)
113 }
114
115 #[inline(always)]
116 fn write_styled(&mut self, text: &str, role: StyleRole, theme: &Theme) -> std::fmt::Result {
117 if !self.color_enabled {
118 self.writer.write_str(text)
119 } else {
120 let style = theme.style(role);
121 write!(self.writer, "{}{}{}", style.render(), text, style.render_reset())
122 }
123 }
124
125 #[inline(always)]
126 fn write_styled_fmt(
127 &mut self,
128 args: std::fmt::Arguments<'_>,
129 role: StyleRole,
130 theme: &Theme,
131 ) -> std::fmt::Result {
132 if !self.color_enabled {
133 self.writer.write_fmt(args)
134 } else {
135 let style = theme.style(role);
136 write!(self.writer, "{}", style.render())?;
137 self.writer.write_fmt(args)?;
138 write!(self.writer, "{}", style.render_reset())
139 }
140 }
141}
142
143pub struct TreeFormatter {
145 config: TreeConfig,
146 visited: HashSet<usize>,
147}
148
149impl TreeFormatter {
150 pub fn new() -> Self {
152 Self::with_config(TreeConfig::default())
153 }
154
155 pub fn with_config(config: TreeConfig) -> Self {
157 Self { config, visited: HashSet::new() }
158 }
159
160 pub fn format(&mut self, value: &ValueRef<'_>) -> String {
163 let is_term = std::io::stdout().is_terminal();
164 let color_enabled = self.config.color_choice.should_color(is_term);
165 let mut output = String::new();
166 self.format_to_styled(value, &mut output, color_enabled).ok();
167 output
168 }
169
170 pub fn format_colored(&mut self, value: &ValueRef<'_>) -> String {
172 let mut output = String::new();
173 self.format_to_styled(value, &mut output, true).ok();
174 output
175 }
176
177 pub fn format_plain(&mut self, value: &ValueRef<'_>) -> String {
179 let mut output = String::new();
180 self.format_to_styled(value, &mut output, false).ok();
181 output
182 }
183
184 pub fn format_to<W: Write>(
186 &mut self,
187 value: &ValueRef<'_>,
188 writer: &mut W,
189 ) -> std::fmt::Result {
190 let is_term = std::io::stdout().is_terminal();
191 let color_enabled = self.config.color_choice.should_color(is_term);
192 self.format_to_styled(value, writer, color_enabled)
193 }
194
195 pub fn format_to_styled<W: Write>(
197 &mut self,
198 value: &ValueRef<'_>,
199 writer: &mut W,
200 color_enabled: bool,
201 ) -> std::fmt::Result {
202 self.visited.clear();
203 let mut styled_writer = StyledWriter { writer, color_enabled };
204 self.format_value(value, &mut styled_writer, "", true, 0)
205 }
206
207 pub fn format_io<W: std::io::Write>(
209 &mut self,
210 value: &ValueRef<'_>,
211 mut writer: W,
212 ) -> std::io::Result<()> {
213 let mut buf = String::new();
214 self.format_to(value, &mut buf).map_err(std::io::Error::other)?;
215 writer.write_all(buf.as_bytes())
216 }
217
218 fn format_value<W: Write>(
219 &mut self,
220 value: &ValueRef<'_>,
221 out: &mut StyledWriter<'_, W>,
222 prefix: &str,
223 is_last: bool,
224 current_depth: usize,
225 ) -> std::fmt::Result {
226 if value.sensitivity() == Sensitivity::Hidden {
228 return Ok(());
229 }
230
231 let (branch, continuation) = if prefix.is_empty() {
232 ("", "")
233 } else if self.config.use_unicode {
234 if is_last { ("└── ", " ") } else { ("├── ", "│ ") }
235 } else {
236 if is_last { ("+-- ", " ") } else { ("|-- ", "| ") }
237 };
238
239 if !prefix.is_empty() {
240 out.write_styled(prefix, StyleRole::Punctuation, &self.config.theme)?;
241 out.write_styled(branch, StyleRole::Punctuation, &self.config.theme)?;
242 }
243
244 if value.sensitivity() == Sensitivity::Secret && self.config.redact_secrets {
246 out.write_styled("[REDACTED]", StyleRole::Sensitive, &self.config.theme)?;
247 out.write_plain("\n")?;
248 return Ok(());
249 }
250
251 if let Some(max_depth) = self.config.max_depth {
253 if current_depth >= max_depth {
254 out.write_styled(
255 "… <max depth reached>",
256 StyleRole::Truncated,
257 &self.config.theme,
258 )?;
259 out.write_plain("\n")?;
260 return Ok(());
261 }
262 }
263
264 match value.kind() {
266 Kind::Unit => {
267 out.write_styled("()", StyleRole::Null, &self.config.theme)?;
268 }
269 Kind::Bool(b) => {
270 out.write_styled_fmt(format_args!("{b}"), StyleRole::Boolean, &self.config.theme)?;
271 }
272 Kind::Char(c) => {
273 out.write_styled("'", StyleRole::Punctuation, &self.config.theme)?;
274 let mut esc_buf = String::new();
275 for esc in c.escape_debug() {
276 esc_buf.push(esc);
277 }
278 out.write_styled(&esc_buf, StyleRole::String, &self.config.theme)?;
279 out.write_styled("'", StyleRole::Punctuation, &self.config.theme)?;
280 }
281 Kind::I8(n) => {
282 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
283 }
284 Kind::I16(n) => {
285 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
286 }
287 Kind::I32(n) => {
288 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
289 }
290 Kind::I64(n) => {
291 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
292 }
293 Kind::I128(n) => {
294 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
295 }
296 Kind::Isize(n) => {
297 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
298 }
299 Kind::U8(n) => {
300 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
301 }
302 Kind::U16(n) => {
303 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
304 }
305 Kind::U32(n) => {
306 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
307 }
308 Kind::U64(n) => {
309 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
310 }
311 Kind::U128(n) => {
312 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
313 }
314 Kind::Usize(n) => {
315 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)?
316 }
317 Kind::F32(f) => {
318 out.write_styled_fmt(format_args!("{f}"), StyleRole::Number, &self.config.theme)?
319 }
320 Kind::F64(f) => {
321 out.write_styled_fmt(format_args!("{f}"), StyleRole::Number, &self.config.theme)?
322 }
323 Kind::Str(s) => {
324 out.write_styled("\"", StyleRole::Punctuation, &self.config.theme)?;
325 let mut esc_buf = String::new();
326 for c in s.chars() {
327 for esc in c.escape_debug() {
328 esc_buf.push(esc);
329 }
330 }
331 out.write_styled(&esc_buf, StyleRole::String, &self.config.theme)?;
332 out.write_styled("\"", StyleRole::Punctuation, &self.config.theme)?;
333 }
334 Kind::Bytes(b) => {
335 out.write_styled("[", StyleRole::Punctuation, &self.config.theme)?;
336 out.write_styled_fmt(
337 format_args!("{} bytes", b.len()),
338 StyleRole::Metadata,
339 &self.config.theme,
340 )?;
341 out.write_styled("]", StyleRole::Punctuation, &self.config.theme)?;
342 }
343 _ => {
344 let type_name = value.type_info().name();
345 if self.config.show_types {
346 out.write_styled(type_name, StyleRole::Type, &self.config.theme)?;
347 }
348
349 if let Some(variant) = value.variant() {
350 if self.config.show_types {
351 out.write_styled("::", StyleRole::Punctuation, &self.config.theme)?;
352 }
353 out.write_styled(variant.name(), StyleRole::Variant, &self.config.theme)?;
354 }
355 }
356 }
357
358 out.write_plain("\n")?;
359
360 if let Some(children) = value.children() {
362 let next_prefix = if prefix.is_empty() {
363 "".to_string()
364 } else {
365 format!("{}{}", prefix, continuation)
366 };
367 self.format_children(children, out, &next_prefix, value.kind(), current_depth + 1)?;
368 }
369
370 Ok(())
371 }
372
373 fn format_children<W: Write>(
374 &mut self,
375 children: &Children<'_>,
376 out: &mut StyledWriter<'_, W>,
377 prefix: &str,
378 parent_kind: &Kind<'_>,
379 current_depth: usize,
380 ) -> std::fmt::Result {
381 match children {
382 Children::Direct(fields) => {
383 let total_count = fields.len();
384 let limit = self.config.max_items.unwrap_or(total_count).min(total_count);
385 let is_truncated = limit < total_count;
386
387 for (idx, (field, child_value)) in fields.iter().take(limit).enumerate() {
388 let is_last = (idx == limit - 1) && !is_truncated;
389
390 let (branch, continuation) = if self.config.use_unicode {
391 if is_last { ("└── ", " ") } else { ("├── ", "│ ") }
392 } else {
393 if is_last { ("+-- ", " ") } else { ("|-- ", "| ") }
394 };
395
396 if field.sensitivity() == Sensitivity::Hidden
398 || child_value.sensitivity() == Sensitivity::Hidden
399 {
400 continue;
401 }
402
403 out.write_styled(prefix, StyleRole::Punctuation, &self.config.theme)?;
404 out.write_styled(branch, StyleRole::Punctuation, &self.config.theme)?;
405
406 if let Some(name) = field.name() {
408 let role = if matches!(parent_kind, Kind::Map) {
409 StyleRole::Key
410 } else {
411 StyleRole::Field
412 };
413 out.write_styled(name, role, &self.config.theme)?;
414 out.write_styled(": ", StyleRole::Punctuation, &self.config.theme)?;
415 } else {
416 out.write_styled("[", StyleRole::Punctuation, &self.config.theme)?;
417 out.write_styled_fmt(
418 format_args!("{}", field.index()),
419 StyleRole::Index,
420 &self.config.theme,
421 )?;
422 out.write_styled("]: ", StyleRole::Punctuation, &self.config.theme)?;
423 }
424
425 if (field.sensitivity() == Sensitivity::Secret
427 || child_value.sensitivity() == Sensitivity::Secret)
428 && self.config.redact_secrets
429 {
430 out.write_styled("[REDACTED]", StyleRole::Sensitive, &self.config.theme)?;
431 out.write_plain("\n")?;
432 continue;
433 }
434
435 if child_value.kind().is_scalar() {
436 self.format_scalar_inline(child_value, out)?;
437 out.write_plain("\n")?;
438 } else {
439 if let Some(max_depth) = self.config.max_depth {
440 if current_depth >= max_depth {
441 out.write_styled(
442 "… <max depth reached>",
443 StyleRole::Truncated,
444 &self.config.theme,
445 )?;
446 out.write_plain("\n")?;
447 continue;
448 }
449 }
450
451 let type_name = child_value.type_info().name();
453 if self.config.show_types {
454 out.write_styled(type_name, StyleRole::Type, &self.config.theme)?;
455 }
456 if let Some(variant) = child_value.variant() {
457 if self.config.show_types {
458 out.write_styled("::", StyleRole::Punctuation, &self.config.theme)?;
459 }
460 out.write_styled(
461 variant.name(),
462 StyleRole::Variant,
463 &self.config.theme,
464 )?;
465 }
466 out.write_plain("\n")?;
467
468 if let Some(sub_children) = child_value.children() {
469 let next_prefix = format!("{}{}", prefix, continuation);
470 self.format_children(
471 sub_children,
472 out,
473 &next_prefix,
474 child_value.kind(),
475 current_depth + 1,
476 )?;
477 }
478 }
479 }
480
481 if is_truncated && self.config.show_truncation {
483 let remaining = total_count - limit;
484 let branch = if self.config.use_unicode { "└── " } else { "\\-- " };
485 out.write_styled(prefix, StyleRole::Punctuation, &self.config.theme)?;
486 out.write_styled(branch, StyleRole::Punctuation, &self.config.theme)?;
487 let notice = format!("… {remaining} more");
488 out.write_styled(¬ice, StyleRole::Truncated, &self.config.theme)?;
489 out.write_plain("\n")?;
490 }
491 }
492 }
493
494 Ok(())
495 }
496
497 fn format_scalar_inline<W: Write>(
498 &self,
499 value: &ValueRef<'_>,
500 out: &mut StyledWriter<'_, W>,
501 ) -> std::fmt::Result {
502 match value.kind() {
503 Kind::Unit => out.write_styled("()", StyleRole::Null, &self.config.theme),
504 Kind::Bool(b) => {
505 out.write_styled_fmt(format_args!("{b}"), StyleRole::Boolean, &self.config.theme)
506 }
507 Kind::Char(c) => {
508 out.write_styled("'", StyleRole::Punctuation, &self.config.theme)?;
509 let mut esc_buf = String::new();
510 for esc in c.escape_debug() {
511 esc_buf.push(esc);
512 }
513 out.write_styled(&esc_buf, StyleRole::String, &self.config.theme)?;
514 out.write_styled("'", StyleRole::Punctuation, &self.config.theme)
515 }
516 Kind::I8(n) => {
517 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
518 }
519 Kind::I16(n) => {
520 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
521 }
522 Kind::I32(n) => {
523 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
524 }
525 Kind::I64(n) => {
526 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
527 }
528 Kind::I128(n) => {
529 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
530 }
531 Kind::Isize(n) => {
532 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
533 }
534 Kind::U8(n) => {
535 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
536 }
537 Kind::U16(n) => {
538 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
539 }
540 Kind::U32(n) => {
541 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
542 }
543 Kind::U64(n) => {
544 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
545 }
546 Kind::U128(n) => {
547 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
548 }
549 Kind::Usize(n) => {
550 out.write_styled_fmt(format_args!("{n}"), StyleRole::Number, &self.config.theme)
551 }
552 Kind::F32(f) => {
553 out.write_styled_fmt(format_args!("{f}"), StyleRole::Number, &self.config.theme)
554 }
555 Kind::F64(f) => {
556 out.write_styled_fmt(format_args!("{f}"), StyleRole::Number, &self.config.theme)
557 }
558 Kind::Str(s) => {
559 out.write_styled("\"", StyleRole::Punctuation, &self.config.theme)?;
560 let mut esc_buf = String::new();
561 for c in s.chars() {
562 for esc in c.escape_debug() {
563 esc_buf.push(esc);
564 }
565 }
566 out.write_styled(&esc_buf, StyleRole::String, &self.config.theme)?;
567 out.write_styled("\"", StyleRole::Punctuation, &self.config.theme)
568 }
569 Kind::Bytes(b) => {
570 out.write_styled("[", StyleRole::Punctuation, &self.config.theme)?;
571 out.write_styled_fmt(
572 format_args!("{} bytes", b.len()),
573 StyleRole::Metadata,
574 &self.config.theme,
575 )?;
576 out.write_styled("]", StyleRole::Punctuation, &self.config.theme)
577 }
578 _ => out.write_styled(value.type_info().name(), StyleRole::Type, &self.config.theme),
579 }
580 }
581}
582
583impl Default for TreeFormatter {
584 fn default() -> Self {
585 Self::new()
586 }
587}