Skip to main content

mockforge_tui/widgets/
json_viewer.rs

1//! Collapsible JSON tree viewer widget.
2
3use ratatui::{
4    layout::Rect,
5    text::{Line, Span},
6    widgets::{Block, Borders, Paragraph},
7    Frame,
8};
9
10use crate::theme::Theme;
11
12/// Render a JSON value as a flat list of indented lines.
13pub fn render(frame: &mut Frame, area: Rect, title: &str, value: &serde_json::Value) {
14    render_scrollable(frame, area, title, value, 0);
15}
16
17/// Render a JSON value with vertical scroll offset.
18pub fn render_scrollable(
19    frame: &mut Frame,
20    area: Rect,
21    title: &str,
22    value: &serde_json::Value,
23    scroll_offset: u16,
24) {
25    let lines = json_to_lines(value, 0);
26
27    let block = Block::default()
28        .title(format!(" {title} "))
29        .title_style(Theme::title())
30        .borders(Borders::ALL)
31        .border_style(Theme::dim())
32        .style(Theme::surface());
33
34    let paragraph = Paragraph::new(lines).block(block).scroll((scroll_offset, 0));
35    frame.render_widget(paragraph, area);
36}
37
38fn json_to_lines(value: &serde_json::Value, depth: usize) -> Vec<Line<'static>> {
39    let indent = "  ".repeat(depth);
40    match value {
41        serde_json::Value::Object(map) => {
42            let mut lines = Vec::new();
43            for (key, val) in map {
44                match val {
45                    serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
46                        lines.push(Line::from(vec![
47                            Span::raw(indent.clone()),
48                            Span::styled(format!("{key}: "), Theme::key_hint()),
49                        ]));
50                        lines.extend(json_to_lines(val, depth + 1));
51                    }
52                    _ => {
53                        lines.push(Line::from(vec![
54                            Span::raw(indent.clone()),
55                            Span::styled(format!("{key}: "), Theme::key_hint()),
56                            value_span(val),
57                        ]));
58                    }
59                }
60            }
61            lines
62        }
63        serde_json::Value::Array(arr) => {
64            let mut lines = Vec::new();
65            for (i, val) in arr.iter().enumerate() {
66                match val {
67                    serde_json::Value::Object(_) | serde_json::Value::Array(_) => {
68                        lines.push(Line::from(vec![
69                            Span::raw(indent.clone()),
70                            Span::styled(format!("[{i}]:"), Theme::dim()),
71                        ]));
72                        lines.extend(json_to_lines(val, depth + 1));
73                    }
74                    _ => {
75                        lines.push(Line::from(vec![
76                            Span::raw(indent.clone()),
77                            Span::styled(format!("[{i}]: "), Theme::dim()),
78                            value_span(val),
79                        ]));
80                    }
81                }
82            }
83            lines
84        }
85        _ => {
86            vec![Line::from(vec![Span::raw(indent), value_span(value)])]
87        }
88    }
89}
90
91fn value_span(value: &serde_json::Value) -> Span<'static> {
92    match value {
93        serde_json::Value::String(s) => {
94            Span::styled(format!("\"{s}\""), ratatui::style::Style::default().fg(Theme::GREEN))
95        }
96        serde_json::Value::Number(n) => {
97            Span::styled(n.to_string(), ratatui::style::Style::default().fg(Theme::PEACH))
98        }
99        serde_json::Value::Bool(b) => {
100            let color = if *b { Theme::GREEN } else { Theme::RED };
101            Span::styled(b.to_string(), ratatui::style::Style::default().fg(color))
102        }
103        serde_json::Value::Null => Span::styled("null", Theme::dim()),
104        _ => Span::raw(value.to_string()),
105    }
106}