Skip to main content

sql_fun_core/
highlighter.rs

1use std::{io::IsTerminal, path::Path, str::FromStr};
2
3/// error related [`HighlighterTheme`]
4#[derive(thiserror::Error, Debug)]
5pub enum HighlighterThemeError {
6    /// Color name invalid
7    #[error("Can not to parse as color : {0}")]
8    ParseColorError(String),
9    /// file IO error
10    #[error(transparent)]
11    IoError(#[from] std::io::Error),
12    /// deserialize error
13    #[error(transparent)]
14    Serde(#[from] toml::de::Error),
15}
16
17/// terminal color settings
18#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, clap::ValueEnum)]
19pub enum TerminalColor {
20    /// enable ANSI Color if stderr is terminal
21    Auto,
22    /// disable ANSI Color (plain text)
23    Never,
24    /// ANSI Color terminal mode
25    Ansi,
26}
27
28impl TerminalColor {
29    /// get non auto value
30    #[must_use]
31    pub fn get_value(&self) -> Self {
32        if matches!(self, Self::Auto) {
33            if std::io::stderr().is_terminal() {
34                Self::Ansi
35            } else {
36                Self::Never
37            }
38        } else {
39            *self
40        }
41    }
42
43    /// return true when self is never
44    #[must_use]
45    pub fn is_never(&self) -> bool {
46        matches!(self, Self::Never)
47    }
48}
49
50/// item of highlighter element
51#[expect(clippy::struct_excessive_bools)]
52#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Default)]
53pub struct HighlighterStyle {
54    foreground: Option<String>,
55    background: Option<String>,
56    #[serde(default, skip_serializing_if = "is_false")]
57    bold: bool,
58    #[serde(default, skip_serializing_if = "is_false")]
59    dimmed: bool,
60    #[serde(default, skip_serializing_if = "is_false")]
61    italic: bool,
62    #[serde(default, skip_serializing_if = "is_false")]
63    underline: bool,
64    #[serde(default, skip_serializing_if = "is_false")]
65    strikethrough: bool,
66    #[serde(default, skip_serializing_if = "is_false")]
67    hidden: bool,
68}
69
70#[expect(clippy::trivially_copy_pass_by_ref)]
71fn is_false(value: &bool) -> bool {
72    !value
73}
74
75impl HighlighterStyle {
76    fn is_default(&self) -> bool {
77        self == &Self::default()
78    }
79}
80
81/// Highlighter theme
82#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
83pub struct HighlighterTheme {
84    #[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
85    identifier: HighlighterStyle,
86    #[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
87    constant: HighlighterStyle,
88    #[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
89    operator: HighlighterStyle,
90    #[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
91    keyword: HighlighterStyle,
92    #[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
93    delimiter: HighlighterStyle,
94    #[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
95    comment: HighlighterStyle,
96}
97
98impl HighlighterTheme {
99    /// deserialize from toml text string
100    ///
101    /// # Errors
102    ///
103    /// [`HighlighterThemeError`] when deserialize error
104    pub fn from_toml_str(toml: &str) -> Result<Self, HighlighterThemeError> {
105        Ok(toml::from_str(toml)?)
106    }
107
108    /// get identifier style
109    #[must_use]
110    pub fn identifier(&self) -> &HighlighterStyle {
111        &self.identifier
112    }
113
114    /// get constant style
115    #[must_use]
116    pub fn constant(&self) -> &HighlighterStyle {
117        &self.constant
118    }
119
120    /// get operator style
121    #[must_use]
122    pub fn operator(&self) -> &HighlighterStyle {
123        &self.operator
124    }
125
126    /// get keyword style
127    #[must_use]
128    pub fn keyword(&self) -> &HighlighterStyle {
129        &self.keyword
130    }
131
132    /// get comment style
133    #[must_use]
134    pub fn comment(&self) -> &HighlighterStyle {
135        &self.comment
136    }
137
138    /// get delimiter style
139    #[must_use]
140    pub fn delimiter(&self) -> &HighlighterStyle {
141        &self.delimiter
142    }
143}
144
145#[cfg(test)]
146mod test_highlighter_theme_from_toml_str {
147    use testresult::TestResult;
148
149    use crate::HighlighterTheme;
150
151    #[rstest::rstest]
152    #[case(
153        r#"
154identifier = { foreground= "blue" }
155constant = { foreground= "blue" }
156comment = { foreground= "green", dimmed = true }
157"#
158    )]
159    fn test_from_toml_str(#[case] toml: &str) -> TestResult {
160        let _theme = HighlighterTheme::from_toml_str(toml)?;
161        Ok(())
162    }
163}
164
165impl HighlighterTheme {
166    /// read toml formatted theme from path
167    ///
168    /// # Errors
169    ///
170    /// [`HighlighterThemeError`] when thema file IO / parse error.
171    pub fn from_toml<P: AsRef<Path>>(path: P) -> Result<Self, HighlighterThemeError> {
172        let toml = fs_err::read_to_string(path)?;
173        Self::from_toml_str(&toml)
174    }
175}
176
177impl HighlighterStyle {
178    /// convert to [`owo_colors::Style`]
179    ///
180    /// # Errors
181    ///
182    /// [`HighlighterThemeError`] when syly item invalid
183    pub fn get_style(&self) -> Result<owo_colors::Style, HighlighterThemeError> {
184        let style = owo_colors::Style::default();
185        let style = if let Some(foreground) = &self.foreground {
186            let fore_color = owo_colors::DynColors::from_str(foreground)
187                .map_err(|_| HighlighterThemeError::ParseColorError(foreground.clone()))?;
188            style.color(fore_color)
189        } else {
190            style
191        };
192        let style = if let Some(background) = &self.background {
193            let back_color = owo_colors::DynColors::from_str(background)
194                .map_err(|_| HighlighterThemeError::ParseColorError(background.clone()))?;
195            style.on_color(back_color)
196        } else {
197            style
198        };
199
200        if self.bold {
201            style.bold()
202        } else {
203            style
204        };
205        let style = if self.dimmed { style.dimmed() } else { style };
206        let style = if self.italic { style.italic() } else { style };
207        let style = if self.strikethrough {
208            style.strikethrough()
209        } else {
210            style
211        };
212        let style = if self.hidden { style.hidden() } else { style };
213
214        Ok(style)
215    }
216}