Skip to main content

sparrow/tui/formatters/
json.rs

1//! Simple JSON pretty-printer with ANSI colors.
2//! No external deps beyond std.
3
4pub struct JsonColors {
5    pub key: &'static str,
6    pub string: &'static str,
7    pub number: &'static str,
8    pub boolean: &'static str,
9    pub null: &'static str,
10    pub bracket: &'static str,
11    pub reset: &'static str,
12}
13
14impl Default for JsonColors {
15    fn default() -> Self {
16        Self {
17            key: "\x1b[38;2;111;166;230m",
18            string: "\x1b[38;2;116;194;88m",
19            number: "\x1b[38;2;242;201;76m",
20            boolean: "\x1b[38;2;86;182;194m",
21            null: "\x1b[38;2;137;125;108m",
22            bracket: "\x1b[38;2;185;176;163m",
23            reset: "\x1b[0m",
24        }
25    }
26}
27
28/// Pretty-print a JSON string with ANSI colors.
29pub fn format_json(json_str: &str) -> String {
30    let colors = JsonColors::default();
31    // Use serde_json to parse and re-serialize with pretty print
32    match serde_json::from_str::<serde_json::Value>(json_str) {
33        Ok(value) => {
34            let pretty =
35                serde_json::to_string_pretty(&value).unwrap_or_else(|_| json_str.to_string());
36            colorize_json(&pretty, &colors)
37        }
38        Err(_) => {
39            // Not valid JSON — return as-is
40            json_str.to_string()
41        }
42    }
43}
44
45/// Apply basic coloring to already-pretty-printed JSON.
46fn colorize_json(json: &str, c: &JsonColors) -> String {
47    let mut out = String::with_capacity(json.len() + 256);
48    let mut in_string = false;
49    let mut after_colon = false;
50    let bytes = json.as_bytes();
51    let mut i = 0;
52
53    while i < bytes.len() {
54        let ch = bytes[i] as char;
55
56        match ch {
57            '"' if !in_string => {
58                in_string = true;
59                // Check if this looks like a key (next non-whitespace is ':')
60                let rest = &json[i + 1..];
61                let key_end = rest.find('"').unwrap_or(rest.len());
62                let after_key = &rest[key_end + 1..];
63                let is_key = after_key.trim_start().starts_with(':');
64                if is_key {
65                    out.push_str(c.key);
66                } else {
67                    out.push_str(c.string);
68                }
69                out.push('"');
70            }
71            '"' if in_string => {
72                out.push('"');
73                out.push_str(c.reset);
74                in_string = false;
75                after_colon = false;
76            }
77            ':' if !in_string => {
78                out.push(':');
79                out.push(' ');
80                after_colon = true;
81            }
82            't' | 'f' if !in_string && !after_colon => {
83                out.push_str(c.boolean);
84                out.push(ch);
85            }
86            'n' if !in_string && !after_colon => {
87                out.push_str(c.null);
88                out.push(ch);
89            }
90            '0'..='9' | '-' if !in_string && after_colon => {
91                out.push_str(c.number);
92                out.push(ch);
93                after_colon = false;
94            }
95            '{' | '}' | '[' | ']' if !in_string => {
96                out.push_str(c.bracket);
97                out.push(ch);
98                out.push_str(c.reset);
99                after_colon = false;
100            }
101            _ => {
102                out.push(ch);
103                if ch == ',' && !in_string {
104                    after_colon = false;
105                }
106                if ch == '\n' {
107                    after_colon = false;
108                }
109            }
110        }
111        i += 1;
112    }
113
114    if in_string {
115        out.push_str(c.reset);
116    }
117    out
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123
124    #[test]
125    fn test_basic_json() {
126        let input = r#"{"name":"test","count":42,"active":true}"#;
127        let output = format_json(input);
128        assert!(output.contains("name"));
129        assert!(output.contains("42"));
130    }
131}