akv_cli/
color.rs

1// Copyright 2025 Heath Stewart.
2// Licensed under the MIT License. See LICENSE.txt in the project root for license information.
3
4use colored_json::Styler;
5use std::env;
6use yansi::{Attribute, Color};
7
8/// Color configuration.
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Config([u16; 8]);
11
12impl Config {
13    pub fn from_env() -> Self {
14        env::var("JQ_COLORS")
15            .ok()
16            .and_then(Config::from_jq)
17            .unwrap_or_default()
18    }
19
20    fn from_jq<S: AsRef<str>>(s: S) -> Option<Self> {
21        let s = s.as_ref();
22        let segments: Vec<&str> = s.split(':').collect();
23
24        // Must have exactly 8 segments
25        if segments.len() != 8 {
26            return None;
27        }
28
29        let mut colors = [0u16; 8];
30        for (i, segment) in segments.iter().enumerate() {
31            let parts: Vec<&str> = segment.split(';').collect();
32
33            // Each segment must have exactly 2 parts (style;color)
34            if parts.len() != 2 {
35                return None;
36            }
37
38            // Parse style and color
39            let style: u8 = parts[0].parse().ok()?;
40            let color_code: u8 = parts[1].parse().ok()?;
41
42            // Map color codes: 30-37 → 0-7, 90-97 → 8-15, 39 → 16 (default)
43            let color = match color_code {
44                30..=37 => color_code - 30,     // 30-37 maps to 0-7
45                39 => 16,                       // 39 maps to 16 (default)
46                90..=97 => color_code - 90 + 8, // 90-97 maps to 8-15
47                _ => return None,               // Invalid color code
48            };
49
50            // Combine style (upper 8 bits) with color (lower 8 bits)
51            colors[i] = ((style as u16) << 8) | (color as u16);
52        }
53
54        Some(Self(colors))
55    }
56
57    /// Gets the color for null.
58    pub fn null(&self) -> Style {
59        Style(self.0[0])
60    }
61
62    /// Gets the color for false.
63    pub fn r#false(&self) -> Style {
64        Style(self.0[1])
65    }
66
67    /// Gets the color for true.
68    pub fn r#true(&self) -> Style {
69        Style(self.0[2])
70    }
71
72    /// Gets the color for numbers.
73    pub fn numbers(&self) -> Style {
74        Style(self.0[3])
75    }
76
77    /// Gets the color for strings.
78    pub fn strings(&self) -> Style {
79        Style(self.0[4])
80    }
81
82    /// Gets the color for arrays.
83    pub fn arrays(&self) -> Style {
84        Style(self.0[5])
85    }
86
87    /// Gets the color for objects.
88    pub fn objects(&self) -> Style {
89        Style(self.0[6])
90    }
91
92    /// Gets the color for object keys.
93    pub fn object_keys(&self) -> Style {
94        Style(self.0[7])
95    }
96}
97
98impl Default for Config {
99    fn default() -> Self {
100        // Default equivalent to "0;90:0;39:0;39:0;39:0;32:1;39:1;39:1;34"
101        // from https://jqlang.org/manual/#colors
102        Self([8, 16, 16, 16, 2, 272, 272, 260])
103    }
104}
105
106impl From<Config> for Styler {
107    fn from(config: Config) -> Self {
108        Self {
109            object_brackets: config.objects().into(),
110            object_colon: config.objects().into(),
111            array_brackets: config.arrays().into(),
112            key: config.object_keys().into(),
113            string_value: config.strings().into(),
114            integer_value: config.numbers().into(),
115            float_value: config.numbers().into(),
116            bool_value: config.r#false().into(),
117            nil_value: config.null().into(),
118            string_include_quotation: true,
119        }
120    }
121}
122
123/// Effects and foreground color.
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct Style(u16);
126
127impl Style {
128    fn attribute(&self) -> Option<Attribute> {
129        // Only supports what jq supports.
130        let effects: u8 = (self.0 >> 8) as u8;
131        match effects {
132            1 => Some(Attribute::Bold),
133            2 => Some(Attribute::Dim),
134            4 => Some(Attribute::Underline),
135            5 => Some(Attribute::Blink),
136            7 => Some(Attribute::Invert),
137            8 => Some(Attribute::Conceal),
138            _ => None,
139        }
140    }
141
142    fn color(&self) -> Color {
143        let fg = self.0 as u8;
144        match fg {
145            16 => Color::Primary,
146            _ => Color::Fixed(fg),
147        }
148    }
149}
150
151impl From<Style> for colored_json::Style {
152    fn from(value: Style) -> Self {
153        let mut style = colored_json::Style::new().fg(value.color());
154        if let Some(attr) = value.attribute() {
155            style = style.attr(attr);
156        }
157        style
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn from_jq_matches_default() {
167        assert_eq!(
168            Config::from_jq("0;90:0;39:0;39:0;39:0;32:1;39:1;39:1;34"),
169            Some(Config::default()),
170        );
171    }
172}