use std::{io::IsTerminal, path::Path, str::FromStr};
#[derive(thiserror::Error, Debug)]
pub enum HighlighterThemeError {
#[error("Can not to parse as color : {0}")]
ParseColorError(String),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
Serde(#[from] toml::de::Error),
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, clap::ValueEnum)]
pub enum TerminalColor {
Auto,
Never,
Ansi,
}
impl TerminalColor {
#[must_use]
pub fn get_value(&self) -> Self {
if matches!(self, Self::Auto) {
if std::io::stderr().is_terminal() {
Self::Ansi
} else {
Self::Never
}
} else {
*self
}
}
#[must_use]
pub fn is_never(&self) -> bool {
matches!(self, Self::Never)
}
}
#[expect(clippy::struct_excessive_bools)]
#[derive(Debug, serde::Serialize, serde::Deserialize, PartialEq, Default)]
pub struct HighlighterStyle {
foreground: Option<String>,
background: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
bold: bool,
#[serde(default, skip_serializing_if = "is_false")]
dimmed: bool,
#[serde(default, skip_serializing_if = "is_false")]
italic: bool,
#[serde(default, skip_serializing_if = "is_false")]
underline: bool,
#[serde(default, skip_serializing_if = "is_false")]
strikethrough: bool,
#[serde(default, skip_serializing_if = "is_false")]
hidden: bool,
}
#[expect(clippy::trivially_copy_pass_by_ref)]
fn is_false(value: &bool) -> bool {
!value
}
impl HighlighterStyle {
fn is_default(&self) -> bool {
self == &Self::default()
}
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
pub struct HighlighterTheme {
#[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
identifier: HighlighterStyle,
#[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
constant: HighlighterStyle,
#[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
operator: HighlighterStyle,
#[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
keyword: HighlighterStyle,
#[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
delimiter: HighlighterStyle,
#[serde(default, skip_serializing_if = "HighlighterStyle::is_default")]
comment: HighlighterStyle,
}
impl HighlighterTheme {
pub fn from_toml_str(toml: &str) -> Result<Self, HighlighterThemeError> {
Ok(toml::from_str(toml)?)
}
#[must_use]
pub fn identifier(&self) -> &HighlighterStyle {
&self.identifier
}
#[must_use]
pub fn constant(&self) -> &HighlighterStyle {
&self.constant
}
#[must_use]
pub fn operator(&self) -> &HighlighterStyle {
&self.operator
}
#[must_use]
pub fn keyword(&self) -> &HighlighterStyle {
&self.keyword
}
#[must_use]
pub fn comment(&self) -> &HighlighterStyle {
&self.comment
}
#[must_use]
pub fn delimiter(&self) -> &HighlighterStyle {
&self.delimiter
}
}
#[cfg(test)]
mod test_highlighter_theme_from_toml_str {
use testresult::TestResult;
use crate::HighlighterTheme;
#[rstest::rstest]
#[case(
r#"
identifier = { foreground= "blue" }
constant = { foreground= "blue" }
comment = { foreground= "green", dimmed = true }
"#
)]
fn test_from_toml_str(#[case] toml: &str) -> TestResult {
let _theme = HighlighterTheme::from_toml_str(toml)?;
Ok(())
}
}
impl HighlighterTheme {
pub fn from_toml<P: AsRef<Path>>(path: P) -> Result<Self, HighlighterThemeError> {
let toml = fs_err::read_to_string(path)?;
Self::from_toml_str(&toml)
}
}
impl HighlighterStyle {
pub fn get_style(&self) -> Result<owo_colors::Style, HighlighterThemeError> {
let style = owo_colors::Style::default();
let style = if let Some(foreground) = &self.foreground {
let fore_color = owo_colors::DynColors::from_str(foreground)
.map_err(|_| HighlighterThemeError::ParseColorError(foreground.clone()))?;
style.color(fore_color)
} else {
style
};
let style = if let Some(background) = &self.background {
let back_color = owo_colors::DynColors::from_str(background)
.map_err(|_| HighlighterThemeError::ParseColorError(background.clone()))?;
style.on_color(back_color)
} else {
style
};
if self.bold {
style.bold()
} else {
style
};
let style = if self.dimmed { style.dimmed() } else { style };
let style = if self.italic { style.italic() } else { style };
let style = if self.strikethrough {
style.strikethrough()
} else {
style
};
let style = if self.hidden { style.hidden() } else { style };
Ok(style)
}
}