feat: Write built in themes to the themes file on first run so users can define custom themes
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
use crate::ui::theme::{Background, Style, Theme, ThemeDefinition};
|
||||
use ratatui::style::Color;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "builtin_themes_tests.rs"]
|
||||
mod builtin_themes_tests;
|
||||
|
||||
pub fn get_builtin_themes() -> Vec<ThemeDefinition> {
|
||||
let watermelon_dark = Theme {
|
||||
background: Some(Background {
|
||||
enabled: Some(false),
|
||||
color: Some(Color::from_str("#233237").unwrap()),
|
||||
}),
|
||||
default: Some(Style {
|
||||
color: Some(Color::from_str("#00FF00").unwrap()),
|
||||
}),
|
||||
downloaded: Some(Style {
|
||||
color: Some(Color::from_str("#80ffbf").unwrap()),
|
||||
}),
|
||||
failure: Some(Style {
|
||||
color: Some(Color::from_str("#ff8080").unwrap()),
|
||||
}),
|
||||
missing: Some(Style {
|
||||
color: Some(Color::from_str("#ff8080").unwrap()),
|
||||
}),
|
||||
primary: Some(Style {
|
||||
color: Some(Color::from_str("#ff19d9").unwrap()),
|
||||
}),
|
||||
secondary: Some(Style {
|
||||
color: Some(Color::from_str("#8c19ff").unwrap()),
|
||||
}),
|
||||
..Theme::default()
|
||||
};
|
||||
let dracula = Theme {
|
||||
background: Some(Background {
|
||||
enabled: Some(false),
|
||||
color: Some(Color::from_str("#233237").unwrap()),
|
||||
}),
|
||||
default: Some(Style {
|
||||
color: Some(Color::from_str("#f8f8f2").unwrap()),
|
||||
}),
|
||||
downloaded: Some(Style {
|
||||
color: Some(Color::from_str("#50fa7b").unwrap()),
|
||||
}),
|
||||
downloading: Some(Style {
|
||||
color: Some(Color::from_str("#f1fa8c").unwrap()),
|
||||
}),
|
||||
failure: Some(Style {
|
||||
color: Some(Color::from_str("#ff5555").unwrap()),
|
||||
}),
|
||||
missing: Some(Style {
|
||||
color: Some(Color::from_str("#ffb86c").unwrap()),
|
||||
}),
|
||||
primary: Some(Style {
|
||||
color: Some(Color::from_str("#ff79c6").unwrap()),
|
||||
}),
|
||||
secondary: Some(Style {
|
||||
color: Some(Color::from_str("#ff79c6").unwrap()),
|
||||
}),
|
||||
unmonitored_missing: Some(Style {
|
||||
color: Some(Color::from_str("#6272a4").unwrap()),
|
||||
}),
|
||||
help: Some(Style {
|
||||
color: Some(Color::from_str("#6272a4").unwrap()),
|
||||
}),
|
||||
success: Some(Style {
|
||||
color: Some(Color::from_str("#50fa7b").unwrap()),
|
||||
}),
|
||||
warning: Some(Style {
|
||||
color: Some(Color::from_str("#f1fa8c").unwrap()),
|
||||
}),
|
||||
unreleased: Some(Style {
|
||||
color: Some(Color::from_str("#f8f8f2").unwrap()),
|
||||
}),
|
||||
..Theme::default()
|
||||
};
|
||||
vec![
|
||||
ThemeDefinition {
|
||||
name: "default".to_owned(),
|
||||
theme: Theme::default(),
|
||||
},
|
||||
ThemeDefinition {
|
||||
name: "watermelon-dark".to_owned(),
|
||||
theme: watermelon_dark,
|
||||
},
|
||||
ThemeDefinition {
|
||||
name: "dracula".to_owned(),
|
||||
theme: dracula,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::builtin_themes::get_builtin_themes;
|
||||
use crate::ui::theme::{Background, Style, Theme, ThemeDefinition};
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::prelude::Color;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_builtin_themes() {
|
||||
let watermelon_dark = Theme {
|
||||
background: Some(Background {
|
||||
enabled: Some(false),
|
||||
color: Some(Color::from_str("#233237").unwrap()),
|
||||
}),
|
||||
default: Some(Style {
|
||||
color: Some(Color::from_str("#00FF00").unwrap()),
|
||||
}),
|
||||
downloaded: Some(Style {
|
||||
color: Some(Color::from_str("#80ffbf").unwrap()),
|
||||
}),
|
||||
failure: Some(Style {
|
||||
color: Some(Color::from_str("#ff8080").unwrap()),
|
||||
}),
|
||||
missing: Some(Style {
|
||||
color: Some(Color::from_str("#ff8080").unwrap()),
|
||||
}),
|
||||
primary: Some(Style {
|
||||
color: Some(Color::from_str("#ff19d9").unwrap()),
|
||||
}),
|
||||
secondary: Some(Style {
|
||||
color: Some(Color::from_str("#8c19ff").unwrap()),
|
||||
}),
|
||||
..Theme::default()
|
||||
};
|
||||
let dracula = Theme {
|
||||
background: Some(Background {
|
||||
enabled: Some(false),
|
||||
color: Some(Color::from_str("#233237").unwrap()),
|
||||
}),
|
||||
default: Some(Style {
|
||||
color: Some(Color::from_str("#f8f8f2").unwrap()),
|
||||
}),
|
||||
downloaded: Some(Style {
|
||||
color: Some(Color::from_str("#50fa7b").unwrap()),
|
||||
}),
|
||||
downloading: Some(Style {
|
||||
color: Some(Color::from_str("#f1fa8c").unwrap()),
|
||||
}),
|
||||
failure: Some(Style {
|
||||
color: Some(Color::from_str("#ff5555").unwrap()),
|
||||
}),
|
||||
missing: Some(Style {
|
||||
color: Some(Color::from_str("#ffb86c").unwrap()),
|
||||
}),
|
||||
primary: Some(Style {
|
||||
color: Some(Color::from_str("#ff79c6").unwrap()),
|
||||
}),
|
||||
secondary: Some(Style {
|
||||
color: Some(Color::from_str("#ff79c6").unwrap()),
|
||||
}),
|
||||
unmonitored_missing: Some(Style {
|
||||
color: Some(Color::from_str("#6272a4").unwrap()),
|
||||
}),
|
||||
help: Some(Style {
|
||||
color: Some(Color::from_str("#6272a4").unwrap()),
|
||||
}),
|
||||
success: Some(Style {
|
||||
color: Some(Color::from_str("#50fa7b").unwrap()),
|
||||
}),
|
||||
warning: Some(Style {
|
||||
color: Some(Color::from_str("#f1fa8c").unwrap()),
|
||||
}),
|
||||
unreleased: Some(Style {
|
||||
color: Some(Color::from_str("#f8f8f2").unwrap()),
|
||||
}),
|
||||
..Theme::default()
|
||||
};
|
||||
let expected_themes = vec![
|
||||
ThemeDefinition {
|
||||
name: "default".to_owned(),
|
||||
theme: Theme::default(),
|
||||
},
|
||||
ThemeDefinition {
|
||||
name: "watermelon-dark".to_owned(),
|
||||
theme: watermelon_dark,
|
||||
},
|
||||
ThemeDefinition {
|
||||
name: "dracula".to_owned(),
|
||||
theme: dracula,
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(expected_themes, get_builtin_themes());
|
||||
}
|
||||
}
|
||||
+13
-8
@@ -28,11 +28,13 @@ use crate::cli::Command;
|
||||
use crate::event::input_event::{Events, InputEvent};
|
||||
use crate::event::Key;
|
||||
use crate::network::{Network, NetworkEvent};
|
||||
use crate::ui::theme::{Theme, ThemeDefinition};
|
||||
use crate::ui::theme::{Theme, ThemeDefinitionsWrapper};
|
||||
use crate::ui::{ui, THEME};
|
||||
use crate::utils::load_theme_config;
|
||||
|
||||
mod app;
|
||||
mod builtin_themes;
|
||||
mod builtin_themes_tests;
|
||||
mod cli;
|
||||
mod event;
|
||||
mod handlers;
|
||||
@@ -80,9 +82,9 @@ struct Cli {
|
||||
global = true,
|
||||
value_parser,
|
||||
env = "MANAGARR_THEME_FILE",
|
||||
help = "The Managarr theme file to use"
|
||||
help = "The Managarr themes file to use"
|
||||
)]
|
||||
theme_file: Option<PathBuf>,
|
||||
themes_file: Option<PathBuf>,
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
@@ -161,7 +163,7 @@ async fn main() -> Result<()> {
|
||||
});
|
||||
start_ui(
|
||||
&app,
|
||||
&args.theme_file,
|
||||
&args.themes_file,
|
||||
args.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||
)
|
||||
.await?;
|
||||
@@ -200,16 +202,19 @@ async fn start_networking(
|
||||
|
||||
async fn start_ui(
|
||||
app: &Arc<Mutex<App<'_>>>,
|
||||
theme_file_arg: &Option<PathBuf>,
|
||||
themes_file_arg: &Option<PathBuf>,
|
||||
theme_name: String,
|
||||
) -> Result<()> {
|
||||
let theme_definitions = if let Some(ref theme_file) = theme_file_arg {
|
||||
let theme_definitions_wrapper = if let Some(ref theme_file) = themes_file_arg {
|
||||
load_theme_config(theme_file.to_str().expect("Invalid theme file specified"))?
|
||||
} else {
|
||||
confy::load("managarr", "themes").unwrap_or_else(|_| vec![ThemeDefinition::default()])
|
||||
confy::load("managarr", "themes").unwrap_or_else(|_| ThemeDefinitionsWrapper::default())
|
||||
};
|
||||
let theme = if !theme_name.is_empty() {
|
||||
let theme_definition = theme_definitions.iter().find(|t| t.name == theme_name);
|
||||
let theme_definition = theme_definitions_wrapper
|
||||
.theme_definitions
|
||||
.iter()
|
||||
.find(|t| t.name == theme_name);
|
||||
|
||||
if theme_definition.is_none() {
|
||||
log_and_print_error(format!("The specified theme was not found: {theme_name}"));
|
||||
|
||||
+36
-2
@@ -1,7 +1,8 @@
|
||||
use crate::builtin_themes::get_builtin_themes;
|
||||
use anyhow::Result;
|
||||
use derivative::Derivative;
|
||||
use ratatui::style::Color;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::str::FromStr;
|
||||
use validate_theme_derive::ValidateTheme;
|
||||
|
||||
@@ -118,6 +119,20 @@ pub struct ThemeDefinition {
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct ThemeDefinitionsWrapper {
|
||||
pub theme_definitions: Vec<ThemeDefinition>,
|
||||
}
|
||||
|
||||
impl Default for ThemeDefinitionsWrapper {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
theme_definitions: get_builtin_themes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_background_color() -> Option<Color> {
|
||||
Some(Color::Rgb(35, 50, 55))
|
||||
}
|
||||
@@ -229,9 +244,28 @@ fn default_warning_style() -> Option<Style> {
|
||||
})
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ThemeDefinitionsWrapper {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let theme_definitions = Vec::<ThemeDefinition>::deserialize(deserializer)?;
|
||||
Ok(ThemeDefinitionsWrapper { theme_definitions })
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ThemeDefinitionsWrapper {
|
||||
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.theme_definitions.serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_color_str<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s: Option<String> = Option::deserialize(deserializer)?;
|
||||
match s {
|
||||
|
||||
+251
-2
@@ -1,7 +1,8 @@
|
||||
mod tests {
|
||||
use crate::ui::theme::{Background, Style, Theme, ThemeDefinition};
|
||||
use pretty_assertions::assert_eq;
|
||||
use crate::ui::theme::{Background, Style, Theme, ThemeDefinition, ThemeDefinitionsWrapper};
|
||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||
use ratatui::style::Color;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_background_default() {
|
||||
@@ -188,4 +189,252 @@ warning:
|
||||
|
||||
assert_eq!(theme, expected_theme);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_definitions_wrapper_default() {
|
||||
let watermelon_dark = Theme {
|
||||
background: Some(Background {
|
||||
enabled: Some(false),
|
||||
color: Some(Color::from_str("#233237").unwrap()),
|
||||
}),
|
||||
default: Some(Style {
|
||||
color: Some(Color::from_str("#00FF00").unwrap()),
|
||||
}),
|
||||
downloaded: Some(Style {
|
||||
color: Some(Color::from_str("#80ffbf").unwrap()),
|
||||
}),
|
||||
failure: Some(Style {
|
||||
color: Some(Color::from_str("#ff8080").unwrap()),
|
||||
}),
|
||||
missing: Some(Style {
|
||||
color: Some(Color::from_str("#ff8080").unwrap()),
|
||||
}),
|
||||
primary: Some(Style {
|
||||
color: Some(Color::from_str("#ff19d9").unwrap()),
|
||||
}),
|
||||
secondary: Some(Style {
|
||||
color: Some(Color::from_str("#8c19ff").unwrap()),
|
||||
}),
|
||||
..Theme::default()
|
||||
};
|
||||
let dracula = Theme {
|
||||
background: Some(Background {
|
||||
enabled: Some(false),
|
||||
color: Some(Color::from_str("#233237").unwrap()),
|
||||
}),
|
||||
default: Some(Style {
|
||||
color: Some(Color::from_str("#f8f8f2").unwrap()),
|
||||
}),
|
||||
downloaded: Some(Style {
|
||||
color: Some(Color::from_str("#50fa7b").unwrap()),
|
||||
}),
|
||||
downloading: Some(Style {
|
||||
color: Some(Color::from_str("#f1fa8c").unwrap()),
|
||||
}),
|
||||
failure: Some(Style {
|
||||
color: Some(Color::from_str("#ff5555").unwrap()),
|
||||
}),
|
||||
missing: Some(Style {
|
||||
color: Some(Color::from_str("#ffb86c").unwrap()),
|
||||
}),
|
||||
primary: Some(Style {
|
||||
color: Some(Color::from_str("#ff79c6").unwrap()),
|
||||
}),
|
||||
secondary: Some(Style {
|
||||
color: Some(Color::from_str("#ff79c6").unwrap()),
|
||||
}),
|
||||
unmonitored_missing: Some(Style {
|
||||
color: Some(Color::from_str("#6272a4").unwrap()),
|
||||
}),
|
||||
help: Some(Style {
|
||||
color: Some(Color::from_str("#6272a4").unwrap()),
|
||||
}),
|
||||
success: Some(Style {
|
||||
color: Some(Color::from_str("#50fa7b").unwrap()),
|
||||
}),
|
||||
warning: Some(Style {
|
||||
color: Some(Color::from_str("#f1fa8c").unwrap()),
|
||||
}),
|
||||
unreleased: Some(Style {
|
||||
color: Some(Color::from_str("#f8f8f2").unwrap()),
|
||||
}),
|
||||
..Theme::default()
|
||||
};
|
||||
let theme_definitions_wrapper = ThemeDefinitionsWrapper {
|
||||
theme_definitions: vec![
|
||||
ThemeDefinition {
|
||||
name: "default".to_owned(),
|
||||
theme: Theme::default(),
|
||||
},
|
||||
ThemeDefinition {
|
||||
name: "watermelon-dark".to_owned(),
|
||||
theme: watermelon_dark,
|
||||
},
|
||||
ThemeDefinition {
|
||||
name: "dracula".to_owned(),
|
||||
theme: dracula,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
ThemeDefinitionsWrapper::default(),
|
||||
theme_definitions_wrapper
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_definitions_wrapper_deserialization() {
|
||||
let theme_definitions = r###"
|
||||
- name: test
|
||||
theme:
|
||||
background:
|
||||
enabled: false
|
||||
color: "#000000"
|
||||
awaiting_import:
|
||||
color: "#000000"
|
||||
indeterminate:
|
||||
color: "#000000"
|
||||
default:
|
||||
color: "#000000"
|
||||
downloaded:
|
||||
color: "#000000"
|
||||
downloading:
|
||||
color: "#000000"
|
||||
failure:
|
||||
color: "#000000"
|
||||
help:
|
||||
color: "#000000"
|
||||
missing:
|
||||
color: "#000000"
|
||||
primary:
|
||||
color: "#000000"
|
||||
secondary:
|
||||
color: "#000000"
|
||||
success:
|
||||
color: "#000000"
|
||||
system_function:
|
||||
color: "#000000"
|
||||
unmonitored:
|
||||
color: "#000000"
|
||||
unmonitored_missing:
|
||||
color: "#000000"
|
||||
unreleased:
|
||||
color: "#000000"
|
||||
warning:
|
||||
color: "#000000"
|
||||
"###;
|
||||
let theme_definition_wrapper: ThemeDefinitionsWrapper =
|
||||
serde_yaml::from_str(theme_definitions).unwrap();
|
||||
let expected_theme_definitions = ThemeDefinitionsWrapper {
|
||||
theme_definitions: vec![ThemeDefinition {
|
||||
name: "test".to_owned(),
|
||||
theme: Theme {
|
||||
background: Some(Background {
|
||||
enabled: Some(false),
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
awaiting_import: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
indeterminate: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
default: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
downloaded: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
downloading: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
failure: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
help: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
missing: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
primary: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
secondary: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
success: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
system_function: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
unmonitored: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
unmonitored_missing: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
unreleased: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
warning: Some(Style {
|
||||
color: Some(Color::Rgb(0, 0, 0)),
|
||||
}),
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(theme_definition_wrapper, expected_theme_definitions);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_theme_definition_wrapper_serialization() {
|
||||
let theme_definition_wrapper = ThemeDefinitionsWrapper {
|
||||
theme_definitions: vec![ThemeDefinition::default()],
|
||||
};
|
||||
let expected_yaml = r###"- name: ''
|
||||
theme:
|
||||
background:
|
||||
color: '#233237'
|
||||
enabled: true
|
||||
awaiting_import:
|
||||
color: '#FFAA42'
|
||||
indeterminate:
|
||||
color: '#FFAA42'
|
||||
default:
|
||||
color: White
|
||||
downloaded:
|
||||
color: Green
|
||||
downloading:
|
||||
color: Magenta
|
||||
failure:
|
||||
color: Red
|
||||
help:
|
||||
color: LightBlue
|
||||
missing:
|
||||
color: Red
|
||||
primary:
|
||||
color: Cyan
|
||||
secondary:
|
||||
color: Yellow
|
||||
success:
|
||||
color: Green
|
||||
system_function:
|
||||
color: Yellow
|
||||
unmonitored:
|
||||
color: Gray
|
||||
unmonitored_missing:
|
||||
color: Yellow
|
||||
unreleased:
|
||||
color: LightCyan
|
||||
warning:
|
||||
color: Magenta
|
||||
"###;
|
||||
|
||||
let serialized_yaml = serde_yaml::to_string(&theme_definition_wrapper).unwrap();
|
||||
|
||||
assert_str_eq!(serialized_yaml, expected_yaml);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::app::{log_and_print_error, App, AppConfig};
|
||||
use crate::cli::{self, Command};
|
||||
use crate::network::Network;
|
||||
use crate::ui::theme::ThemeDefinition;
|
||||
use crate::ui::theme::ThemeDefinitionsWrapper;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "utils_tests.rs"]
|
||||
@@ -154,7 +154,7 @@ pub(super) fn load_config(path: &str) -> Result<AppConfig> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn load_theme_config(path: &str) -> Result<Vec<ThemeDefinition>> {
|
||||
pub(super) fn load_theme_config(path: &str) -> Result<ThemeDefinitionsWrapper> {
|
||||
match File::open(path).map_err(|e| anyhow!(e)) {
|
||||
Ok(file) => {
|
||||
let reader = BufReader::new(file);
|
||||
|
||||
Reference in New Issue
Block a user