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