Skip to main content

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