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 = serde_json::to_string_pretty(&value).unwrap_or_else(|_| json_str.to_string());
35            colorize_json(&pretty, &colors)
36        }
37        Err(_) => {
38            // Not valid JSON — return as-is
39            json_str.to_string()
40        }
41    }
42}
43
44/// Apply basic coloring to already-pretty-printed JSON.
45fn colorize_json(json: &str, c: &JsonColors) -> String {
46    let mut out = String::with_capacity(json.len() + 256);
47    let mut in_string = false;
48    let mut after_colon = false;
49    let bytes = json.as_bytes();
50    let mut i = 0;
51
52    while i < bytes.len() {
53        let ch = bytes[i] as char;
54
55        match ch {
56            '"' if !in_string => {
57                in_string = true;
58                // Check if this looks like a key (next non-whitespace is ':')
59                let rest = &json[i + 1..];
60                let key_end = rest.find('"').unwrap_or(rest.len());
61                let after_key = &rest[key_end + 1..];
62                let is_key = after_key.trim_start().starts_with(':');
63                if is_key {
64                    out.push_str(c.key);
65                } else {
66                    out.push_str(c.string);
67                }
68                out.push('"');
69            }
70            '"' if in_string => {
71                out.push('"');
72                out.push_str(c.reset);
73                in_string = false;
74                after_colon = false;
75            }
76            ':' if !in_string => {
77                out.push(':');
78                out.push(' ');
79                after_colon = true;
80            }
81            't' | 'f' if !in_string && !after_colon => {
82                out.push_str(c.boolean);
83                out.push(ch);
84            }
85            'n' if !in_string && !after_colon => {
86                out.push_str(c.null);
87                out.push(ch);
88            }
89            '0'..='9' | '-' if !in_string && after_colon => {
90                out.push_str(c.number);
91                out.push(ch);
92                after_colon = false;
93            }
94            '{' | '}' | '[' | ']' if !in_string => {
95                out.push_str(c.bracket);
96                out.push(ch);
97                out.push_str(c.reset);
98                after_colon = false;
99            }
100            _ => {
101                out.push(ch);
102                if ch == ',' && !in_string {
103                    after_colon = false;
104                }
105                if ch == '\n' {
106                    after_colon = false;
107                }
108            }
109        }
110        i += 1;
111    }
112
113    if in_string {
114        out.push_str(c.reset);
115    }
116    out
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn test_basic_json() {
125        let input = r#"{"name":"test","count":42,"active":true}"#;
126        let output = format_json(input);
127        assert!(output.contains("name"));
128        assert!(output.contains("42"));
129    }
130}