feat: Initial support for custom user-defined themes

This commit is contained in:
2025-03-04 18:09:09 -07:00
parent 847de75713
commit 5cb60c317d
11 changed files with 582 additions and 67 deletions
+233
View File
@@ -0,0 +1,233 @@
use anyhow::Result;
use derivative::Derivative;
use ratatui::style::Color;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[cfg(test)]
#[path = "theme_tests.rs"]
mod theme_tests;
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Derivative)]
#[derivative(Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Background {
#[serde(
deserialize_with = "deserialize_color_str",
serialize_with = "serialize_color_str",
default = "default_background_color"
)]
#[derivative(Default(value = "Some(Color::Rgb(35, 50, 55))"))]
pub color: Option<Color>,
#[derivative(Default(value = "Some(true)"))]
#[serde(default = "default_background_enabled")]
pub enabled: Option<bool>,
}
#[derive(Debug, Deserialize, Serialize, Default, Clone, Copy)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Style {
#[serde(
deserialize_with = "deserialize_color_str",
serialize_with = "serialize_color_str"
)]
pub color: Option<Color>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Derivative)]
#[derivative(Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct Theme {
#[serde(default = "default_background")]
#[derivative(Default(
value = "Some(Background { color: Some(Color::Rgb(35, 50, 55)), enabled: Some(true) })"
))]
pub background: Option<Background>,
#[serde(default = "default_awaiting_import_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Rgb(255, 170, 66)) })"))]
pub awaiting_import: Option<Style>,
#[serde(default = "default_indeterminate_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Rgb(255, 170, 66)) })"))]
pub indeterminate: Option<Style>,
#[serde(default = "default_default_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::White) })"))]
pub default: Option<Style>,
#[serde(default = "default_downloaded_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Green) })"))]
pub downloaded: Option<Style>,
#[serde(default = "default_downloading_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Magenta) })"))]
pub downloading: Option<Style>,
#[serde(default = "default_failure_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Red) })"))]
pub failure: Option<Style>,
#[serde(default = "default_help_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::LightBlue) })"))]
pub help: Option<Style>,
#[serde(default = "default_missing_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Red) })"))]
pub missing: Option<Style>,
#[serde(default = "default_primary_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Cyan) })"))]
pub primary: Option<Style>,
#[serde(default = "default_secondary_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
pub secondary: Option<Style>,
#[serde(default = "default_success_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Green) })"))]
pub success: Option<Style>,
#[serde(default = "default_system_function_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
pub system_function: Option<Style>,
#[serde(default = "default_unmonitored_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Gray) })"))]
pub unmonitored: Option<Style>,
#[serde(default = "default_unmonitored_missing_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
pub unmonitored_missing: Option<Style>,
#[serde(default = "default_unreleased_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::LightCyan) })"))]
pub unreleased: Option<Style>,
#[serde(default = "default_warning_style")]
#[derivative(Default(value = "Some(Style { color: Some(Color::Magenta) })"))]
pub warning: Option<Style>,
}
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
pub struct ThemeDefinition {
pub name: String,
#[serde(default)]
pub theme: Theme,
}
fn default_background_color() -> Option<Color> {
Some(Color::Rgb(35, 50, 55))
}
fn default_background_enabled() -> Option<bool> {
Some(true)
}
fn default_background() -> Option<Background> {
Some(Background {
color: default_background_color(),
enabled: Some(true),
})
}
fn default_awaiting_import_style() -> Option<Style> {
Some(Style {
color: Some(Color::Rgb(255, 170, 66)),
})
}
fn default_indeterminate_style() -> Option<Style> {
Some(Style {
color: Some(Color::Rgb(255, 170, 66)),
})
}
fn default_default_style() -> Option<Style> {
Some(Style {
color: Some(Color::White),
})
}
fn default_downloaded_style() -> Option<Style> {
Some(Style {
color: Some(Color::Green),
})
}
fn default_downloading_style() -> Option<Style> {
Some(Style {
color: Some(Color::Magenta),
})
}
fn default_failure_style() -> Option<Style> {
Some(Style {
color: Some(Color::Red),
})
}
fn default_help_style() -> Option<Style> {
Some(Style {
color: Some(Color::LightBlue),
})
}
fn default_missing_style() -> Option<Style> {
Some(Style {
color: Some(Color::Red),
})
}
fn default_primary_style() -> Option<Style> {
Some(Style {
color: Some(Color::Cyan),
})
}
fn default_secondary_style() -> Option<Style> {
Some(Style {
color: Some(Color::Yellow),
})
}
fn default_success_style() -> Option<Style> {
Some(Style {
color: Some(Color::Green),
})
}
fn default_system_function_style() -> Option<Style> {
Some(Style {
color: Some(Color::Yellow),
})
}
fn default_unmonitored_style() -> Option<Style> {
Some(Style {
color: Some(Color::Gray),
})
}
fn default_unmonitored_missing_style() -> Option<Style> {
Some(Style {
color: Some(Color::Yellow),
})
}
fn default_unreleased_style() -> Option<Style> {
Some(Style {
color: Some(Color::LightCyan),
})
}
fn default_warning_style() -> Option<Style> {
Some(Style {
color: Some(Color::Magenta),
})
}
fn deserialize_color_str<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s: Option<String> = Option::deserialize(deserializer)?;
match s {
Some(s) => Color::from_str(&s)
.map_err(serde::de::Error::custom)
.map(Some),
None => Ok(None),
}
}
fn serialize_color_str<S>(color: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&color.unwrap().to_string())
}