Skip to main content

rusty_rich/
pretty.rs

1//! Pretty-printing for Rust data structures with Rich styling.
2//!
3//! Equivalent to Python Rich's `pretty.py`. Provides a node-tree traversal
4//! system that can render structured data (debug output, JSON) with
5//! indentation guides, syntax highlighting, and configurable depth limits.
6
7use crate::console::{ConsoleOptions, RenderResult, Renderable};
8#[cfg(feature = "syntax-highlighting")]
9use crate::highlighter::ReprHighlighter;
10use crate::segment::Segment;
11use crate::text::Text;
12
13// ---------------------------------------------------------------------------
14// Node
15// ---------------------------------------------------------------------------
16
17/// A node in a pretty-print tree.
18#[derive(Debug, Clone)]
19pub struct Node {
20    /// Optional key (e.g. struct field name, map key).
21    pub key: Option<String>,
22    /// Optional value string (leaf nodes).
23    pub value: Option<String>,
24    /// Child nodes.
25    pub children: Vec<Node>,
26    /// Whether this node is a container (struct, map, array).
27    pub is_container: bool,
28    /// Whether this node is an iterable.
29    pub is_iter: bool,
30    /// Whether this node is a mapping.
31    pub is_mapping: bool,
32    /// Whether this node has `__attrs__`-style fields.
33    pub is_attrs: bool,
34    /// Opening bracket string.
35    pub open_brace: String,
36    /// Closing bracket string.
37    pub close_brace: String,
38    /// Text shown when empty.
39    pub empty: String,
40    /// Whether this is the last sibling.
41    pub last: bool,
42}
43
44impl Node {
45    /// Create a new Node with an optional key and value.
46    pub fn new(key: Option<String>, value: Option<String>) -> Self {
47        Self {
48            key,
49            value,
50            children: Vec::new(),
51            is_container: false,
52            is_iter: false,
53            is_mapping: false,
54            is_attrs: false,
55            open_brace: "(".to_string(),
56            close_brace: ")".to_string(),
57            empty: String::new(),
58            last: true,
59        }
60    }
61
62    /// Add a child node.
63    pub fn add_child(&mut self, child: Node) {
64        self.children.push(child);
65    }
66}
67
68// ---------------------------------------------------------------------------
69// Pretty
70// ---------------------------------------------------------------------------
71
72/// Pretty renderable that traverses a [`Node`] tree.
73///
74/// Renders structured data with indentation guides, configurable depth,
75/// string length limits, and syntax highlighting via [`ReprHighlighter`].
76#[derive(Debug, Clone)]
77pub struct Pretty {
78    /// The root node of the tree to render.
79    node: Node,
80    /// Whether to draw indentation guides (vertical lines between siblings).
81    indent_guides: bool,
82    /// Maximum nesting depth (None = unlimited).
83    max_depth: Option<usize>,
84    /// Maximum string length before truncation.
85    max_string: Option<usize>,
86    /// Maximum number of children to show per container.
87    max_length: Option<usize>,
88    /// If true, expand all containers regardless of depth.
89    expand_all: bool,
90    /// Highlighter for values.
91    #[cfg(feature = "syntax-highlighting")]
92    highlighter: ReprHighlighter,
93}
94
95impl Pretty {
96    /// Create a new Pretty renderable from a [`Node`] tree.
97    pub fn new(node: Node) -> Self {
98        Self {
99            node,
100            indent_guides: true,
101            max_depth: None,
102            max_string: None,
103            max_length: None,
104            expand_all: false,
105            #[cfg(feature = "syntax-highlighting")]
106            highlighter: ReprHighlighter::new(),
107        }
108    }
109
110    /// Builder: enable or disable indent guides.
111    pub fn indent_guides(mut self, value: bool) -> Self {
112        self.indent_guides = value;
113        self
114    }
115
116    /// Builder: set the maximum nesting depth.
117    pub fn max_depth(mut self, depth: usize) -> Self {
118        self.max_depth = Some(depth);
119        self
120    }
121
122    /// Builder: set the maximum string length before truncation.
123    pub fn max_string(mut self, max: usize) -> Self {
124        self.max_string = Some(max);
125        self
126    }
127
128    /// Builder: set the maximum number of children to show per container.
129    pub fn max_length(mut self, max: usize) -> Self {
130        self.max_length = Some(max);
131        self
132    }
133
134    /// Builder: expand all containers regardless of depth limit.
135    pub fn expand_all(mut self) -> Self {
136        self.expand_all = true;
137        self
138    }
139
140    /// Generate a Node tree from an arbitrary value using the [`Debug`] trait.
141    ///
142    /// Parses the `{:#?}` debug representation into a structured [`Node`] tree.
143    pub fn from_debug<T: std::fmt::Debug>(value: &T) -> Self {
144        let debug_str = format!("{:#?}", value);
145        let node = parse_debug_to_node(&debug_str);
146        Self::new(node)
147    }
148
149    /// Create a Pretty from a `serde_json::Value`.
150    pub fn from_json(value: &serde_json::Value) -> Self {
151        let node = json_to_node(value, None);
152        Self::new(node)
153    }
154}
155
156impl Renderable for Pretty {
157    fn render(&self, options: &ConsoleOptions) -> RenderResult {
158        let mut lines: Vec<Vec<Segment>> = Vec::new();
159        let prefix = String::new();
160        let depth = 0;
161        self.render_node(&self.node, &mut lines, &prefix, depth, options);
162        RenderResult {
163            lines,
164            items: Vec::new(),
165        }
166    }
167}
168
169impl Pretty {
170    fn render_node(
171        &self,
172        node: &Node,
173        lines: &mut Vec<Vec<Segment>>,
174        prefix: &str,
175        depth: usize,
176        options: &ConsoleOptions,
177    ) {
178        // Check depth limit
179        if let Some(max) = self.max_depth {
180            if depth > max && !self.expand_all {
181                lines.push(vec![
182                    Segment::new(prefix),
183                    Segment::new("..."),
184                    Segment::line(),
185                ]);
186                return;
187            }
188        }
189
190        let indent = "    ";
191        let guide = if self.indent_guides && !options.ascii_only {
192            "│   "
193        } else {
194            "    "
195        };
196
197        let mut line_text = String::from(prefix);
198
199        // Key
200        if let Some(ref key) = node.key {
201            #[cfg(feature = "syntax-highlighting")]
202            {
203                let highlighted = self.highlighter.highlight_str(key);
204                line_text.push_str(&highlighted.plain);
205            }
206            #[cfg(not(feature = "syntax-highlighting"))]
207            {
208                line_text.push_str(key);
209            }
210            line_text.push_str(": ");
211        }
212
213        // Value or container
214        if node.children.is_empty() {
215            if let Some(ref value) = node.value {
216                let truncated = if let Some(max) = self.max_string {
217                    if value.len() > max {
218                        format!("{}...", &value[..max])
219                    } else {
220                        value.clone()
221                    }
222                } else {
223                    value.clone()
224                };
225                #[cfg(feature = "syntax-highlighting")]
226                let highlighted = self.highlighter.highlight_str(&truncated);
227                #[cfg(feature = "syntax-highlighting")]
228                {
229                    line_text.push_str(&highlighted.plain);
230                }
231                #[cfg(not(feature = "syntax-highlighting"))]
232                {
233                    line_text.push_str(&truncated);
234                }
235            }
236            lines.push(vec![Segment::new(&line_text), Segment::line()]);
237        } else {
238            // Opening brace
239            line_text.push_str(&node.open_brace);
240            lines.push(vec![Segment::new(&line_text), Segment::line()]);
241
242            let max_len = self.max_length.unwrap_or(usize::MAX);
243            let count = node.children.len();
244            let show_ellipsis = count > max_len;
245
246            for (i, child) in node.children.iter().enumerate() {
247                if i >= max_len {
248                    if show_ellipsis {
249                        let child_prefix = format!("{prefix}{indent}");
250                        lines.push(vec![
251                            Segment::new(format!("{child_prefix}... ({} more)", count - max_len)),
252                            Segment::line(),
253                        ]);
254                    }
255                    break;
256                }
257                let child_prefix = if self.indent_guides && i < count - 1 {
258                    format!("{prefix}{guide}")
259                } else {
260                    format!("{prefix}{indent}")
261                };
262                self.render_node(child, lines, &child_prefix, depth + 1, options);
263            }
264
265            // Closing brace
266            lines.push(vec![
267                Segment::new(format!("{prefix}{}", node.close_brace)),
268                Segment::line(),
269            ]);
270        }
271    }
272}
273
274/// Install Pretty as the default display handler.
275///
276/// This is a no-op in Rust (the Rust standard library handles this via
277/// the `Display` and `Debug` traits), but is provided for API
278/// compatibility with Python Rich's `pretty.install()`.
279pub fn install() {}
280
281/// Pretty-print a value to the console.
282pub fn pprint<T: std::fmt::Debug>(value: &T, console: &mut crate::console::Console) {
283    let pretty = Pretty::from_debug(value);
284    console.println(&pretty);
285}
286
287/// Generate a pretty [`Text`] representation of a value.
288pub fn pretty_repr<T: std::fmt::Debug>(value: &T) -> Text {
289    let debug_str = format!("{:#?}", value);
290    #[cfg(feature = "syntax-highlighting")]
291    {
292        let highlighter = ReprHighlighter::new();
293        highlighter.highlight_str(&debug_str)
294    }
295    #[cfg(not(feature = "syntax-highlighting"))]
296    {
297        let mut t = Text::new("");
298        t.plain = debug_str;
299        t
300    }
301}
302
303/// Traverse an arbitrary value and build a [`Node`] tree.
304///
305/// Uses the [`Debug`] trait to introspect the value.
306pub fn traverse(value: &dyn std::fmt::Debug) -> Node {
307    let debug_str = format!("{:#?}", value);
308    parse_debug_to_node(&debug_str)
309}
310
311// ---------------------------------------------------------------------------
312// Internal helpers
313// ---------------------------------------------------------------------------
314
315/// Parse a `{:#?}` debug string into a [`Node`] tree.
316fn parse_debug_to_node(debug: &str) -> Node {
317    let lines: Vec<&str> = debug.lines().collect();
318    if lines.is_empty() {
319        return Node::new(None, Some(String::new()));
320    }
321
322    // Single-line value
323    if lines.len() == 1 {
324        return Node::new(None, Some(lines[0].trim().to_string()));
325    }
326
327    // Multi-line: parse structure
328    let trimmed = debug.trim();
329    let (open_brace, close_brace) = if trimmed.starts_with('{') {
330        ("{", "}")
331    } else if trimmed.starts_with('[') {
332        ("[", "]")
333    } else {
334        ("(", ")")
335    };
336
337    let mut node = Node::new(None, None);
338    node.open_brace = open_brace.to_string();
339    node.close_brace = close_brace.to_string();
340    node.is_container = true;
341
342    // Extract children from indented lines
343    for line in lines.iter().skip(1) {
344        let trimmed_line = line.trim();
345        if trimmed_line.is_empty() || trimmed_line == close_brace {
346            continue;
347        }
348        // Try to parse key: value
349        if let Some(idx) = trimmed_line.find(": ") {
350            let key = trimmed_line[..idx].trim().to_string();
351            let value = trimmed_line[idx + 2..].trim().to_string();
352            let child = Node::new(Some(key), Some(value));
353            node.children.push(child);
354        } else {
355            let child = Node::new(None, Some(trimmed_line.to_string()));
356            node.children.push(child);
357        }
358    }
359
360    node
361}
362
363/// Convert a `serde_json::Value` into a [`Node`] tree.
364fn json_to_node(value: &serde_json::Value, key: Option<String>) -> Node {
365    match value {
366        serde_json::Value::Null => {
367            let mut node = Node::new(key, Some("null".to_string()));
368            node.is_container = false;
369            node
370        }
371        serde_json::Value::Bool(b) => {
372            let mut node = Node::new(key, Some(b.to_string()));
373            node.is_container = false;
374            node
375        }
376        serde_json::Value::Number(n) => {
377            let mut node = Node::new(key, Some(n.to_string()));
378            node.is_container = false;
379            node
380        }
381        serde_json::Value::String(s) => {
382            let display = format!("\"{}\"", s);
383            let mut node = Node::new(key, Some(display));
384            node.is_container = false;
385            node
386        }
387        serde_json::Value::Array(arr) => {
388            let mut node = Node::new(key, None);
389            node.is_container = true;
390            node.is_iter = true;
391            node.open_brace = "[".to_string();
392            node.close_brace = "]".to_string();
393            node.empty = "[]".to_string();
394            for item in arr {
395                node.children.push(json_to_node(item, None));
396            }
397            node
398        }
399        serde_json::Value::Object(obj) => {
400            let mut node = Node::new(key, None);
401            node.is_container = true;
402            node.is_mapping = true;
403            node.open_brace = "{".to_string();
404            node.close_brace = "}".to_string();
405            node.empty = "{}".to_string();
406            for (k, v) in obj {
407                node.children.push(json_to_node(v, Some(k.clone())));
408            }
409            node
410        }
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417    use crate::console::ConsoleOptions;
418
419    #[test]
420    fn test_pretty_from_debug() {
421        let value = vec!["hello", "world"];
422        let pretty = Pretty::from_debug(&value);
423        let opts = ConsoleOptions::default();
424        let result = pretty.render(&opts);
425        let ansi = result.to_ansi();
426        assert!(ansi.contains("hello"));
427        assert!(ansi.contains("world"));
428    }
429
430    #[test]
431    fn test_pretty_from_json() {
432        let value = serde_json::json!({"name": "Alice", "age": 30});
433        let pretty = Pretty::from_json(&value);
434        let opts = ConsoleOptions::default();
435        let result = pretty.render(&opts);
436        let ansi = result.to_ansi();
437        assert!(ansi.contains("Alice"));
438        assert!(ansi.contains("30"));
439    }
440
441    #[test]
442    fn test_pretty_repr() {
443        let text = pretty_repr(&42);
444        assert!(!text.plain.is_empty());
445    }
446
447    #[test]
448    fn test_max_depth() {
449        let inner = serde_json::json!({"a": {"b": {"c": 1}}});
450        let pretty = Pretty::from_json(&inner).max_depth(1);
451        let opts = ConsoleOptions::default();
452        let result = pretty.render(&opts);
453        let ansi = result.to_ansi();
454        // Should contain the depth truncation marker
455        assert!(ansi.contains("...") || ansi.contains("1"));
456    }
457
458    #[test]
459    fn test_json_to_node_empty_object() {
460        let value = serde_json::Value::Object(serde_json::Map::new());
461        let node = json_to_node(&value, None);
462        assert!(node.is_container);
463        assert!(node.children.is_empty());
464    }
465
466    #[test]
467    fn test_json_to_node_empty_array() {
468        let value = serde_json::Value::Array(Vec::new());
469        let node = json_to_node(&value, None);
470        assert!(node.is_container);
471        assert!(node.children.is_empty());
472    }
473
474    #[test]
475    fn test_json_to_node_scalars() {
476        let null_node = json_to_node(&serde_json::Value::Null, None);
477        assert_eq!(null_node.value.as_deref(), Some("null"));
478
479        let bool_node = json_to_node(&serde_json::Value::Bool(true), Some("flag".into()));
480        assert_eq!(bool_node.key.as_deref(), Some("flag"));
481        assert_eq!(bool_node.value.as_deref(), Some("true"));
482
483        let num_node = json_to_node(&serde_json::json!(42), None);
484        assert_eq!(num_node.value.as_deref(), Some("42"));
485
486        let str_node = json_to_node(&serde_json::json!("hello"), None);
487        assert!(str_node.value.as_deref().unwrap_or("").contains("hello"));
488    }
489
490    #[test]
491    fn test_install_is_noop() {
492        // Just verify it doesn't panic
493        install();
494    }
495
496    #[test]
497    fn test_traverse() {
498        let node = traverse(&"test");
499        assert!(node.value.is_some());
500    }
501
502    #[test]
503    fn test_builder_methods() {
504        let node = Node::new(None, Some("value".to_string()));
505        let pretty = Pretty::new(node)
506            .indent_guides(false)
507            .max_depth(5)
508            .max_string(100)
509            .max_length(10)
510            .expand_all();
511        assert!(!pretty.indent_guides);
512        assert_eq!(pretty.max_depth, Some(5));
513        assert_eq!(pretty.max_string, Some(100));
514        assert_eq!(pretty.max_length, Some(10));
515        assert!(pretty.expand_all);
516    }
517}