feat: Write built in themes to the themes file on first run so users can define custom themes
@@ -206,6 +206,9 @@ Key:
|
||||
|
||||
- [ ] Support for Tautulli
|
||||
|
||||
### Themes
|
||||
Managarr ships with a few themes out of the box. See the [Themes README](themes/README.md) page for more information.
|
||||
|
||||
### The Managarr CLI
|
||||
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
|
||||
|
||||
@@ -315,6 +318,7 @@ managarr --config-file /path/to/config.yml
|
||||
|
||||
### Example Configuration:
|
||||
```yaml
|
||||
theme: default
|
||||
radarr:
|
||||
- host: 192.168.0.78
|
||||
port: 7878
|
||||
@@ -357,6 +361,7 @@ tautulli:
|
||||
|
||||
### Example Multi-Instance Configuration:
|
||||
```yaml
|
||||
theme: default
|
||||
radarr:
|
||||
- host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1'
|
||||
port: 7878
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{parse_macro_input, Data, DeriveInput, Fields};
|
||||
use syn::{Data, DeriveInput, Fields, parse_macro_input};
|
||||
|
||||
/// Derive macro for generating a `validate` method for a Theme struct.
|
||||
/// The `validate` method ensures that all values with the `validate` attribute are not `None`.
|
||||
@@ -77,9 +77,10 @@ pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
|
||||
for field in &fields.named {
|
||||
let field_name = &field.ident;
|
||||
|
||||
let has_validate_attr = field.attrs.iter().any(|attr| {
|
||||
attr.path().is_ident("validate")
|
||||
});
|
||||
let has_validate_attr = field
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().is_ident("validate"));
|
||||
|
||||
if has_validate_attr {
|
||||
validation_checks.push(quote! {
|
||||
@@ -100,5 +101,6 @@ pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
|
||||
#(#validation_checks)*
|
||||
}
|
||||
}
|
||||
}.into()
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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}"));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Managarr Themes
|
||||
The Managarr TUI can be customized to look how you like with various themes.
|
||||
|
||||
There are a few themes included by default with Managarr and are added to your `themes.yml`
|
||||
on first startup. You can simply add more custom themes as you wish to this file.
|
||||
|
||||
## Table of Contents
|
||||
- [Built In Themes](#built-in-themes)
|
||||
- [Creating a Custom Theme](#creating-a-custom-theme)
|
||||
|
||||
## Built-In Themes
|
||||
Managarr ships with a handful of built-in themes that you can either use or base your own
|
||||
custom theme off of. The following themes are included by default:
|
||||
|
||||
### [Default](./default/README.md)
|
||||

|
||||
|
||||
### [Dracula](./dracula/README.md)
|
||||

|
||||
|
||||
### [Watermelon Dark](./watermelon-dark/README.md)
|
||||

|
||||
|
||||
## Creating a Custom Theme
|
||||
To create a custom theme, you will need to add a new entry to the `themes.yml` file. If you decide not to add it to the
|
||||
`themes.yml` file, you can also specify a different file to load themes from using the `--themes-file` argument.
|
||||
|
||||
Themes are customizable using hex color codes for the various elements of the TUI. The following
|
||||
is an example that shows every available customization option for a custom theme:
|
||||
|
||||
```yaml
|
||||
- name: my-theme
|
||||
theme:
|
||||
background:
|
||||
enabled: true # Disable for transparent backgrounds
|
||||
color: "#233237"
|
||||
awaiting_import:
|
||||
color: "#FFAA42"
|
||||
indeterminate:
|
||||
color: "#FFAA42"
|
||||
default:
|
||||
color: "#FFFFFF"
|
||||
downloaded:
|
||||
color: "#00FF00"
|
||||
downloading:
|
||||
color: "#762671"
|
||||
failure:
|
||||
color: "#DE382B"
|
||||
help:
|
||||
color: "#00FFFF"
|
||||
missing:
|
||||
color: "#DE382B"
|
||||
primary:
|
||||
color: "#2CB5E9"
|
||||
secondary:
|
||||
color: "#FFC706"
|
||||
success:
|
||||
color: "#39B54A"
|
||||
system_function:
|
||||
color: "#FFC706"
|
||||
unmonitored:
|
||||
color: "#808080"
|
||||
unmonitored_missing:
|
||||
color: "#FFC706"
|
||||
unreleased:
|
||||
color: "#00FFFF"
|
||||
warning:
|
||||
color: "#FF00FF"
|
||||
```
|
||||
|
||||
In order to activate your custom theme, you can either update your `config.yml`:
|
||||
|
||||
```yaml
|
||||
theme: my-theme
|
||||
radarr:
|
||||
...
|
||||
sonarr:
|
||||
...
|
||||
```
|
||||
|
||||
Or you can test out your theme via the `--theme` flag on the CLI:
|
||||
|
||||
```shell
|
||||
managarr --theme my-theme
|
||||
```
|
||||
|
||||
If you're developing your own theme and don't want to add it to the main `themes.yml` file, you can
|
||||
also use the `--themes-file` argument to specify a different file to load themes from:
|
||||
|
||||
```shell
|
||||
managarr --themes-file /path/to/my/testing-themes.yml
|
||||
```
|
||||
@@ -0,0 +1,9 @@
|
||||
# Managarr Default Theme
|
||||
The [themes.yml](./themes.yml) file in this directory corresponds to the theme configuration for the
|
||||
default Managarr theme.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||
|
After Width: | Height: | Size: 340 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 217 KiB |
@@ -0,0 +1,37 @@
|
||||
- name: default
|
||||
theme:
|
||||
background:
|
||||
enabled: true
|
||||
color: "#233237"
|
||||
awaiting_import:
|
||||
color: "#FFAA42"
|
||||
indeterminate:
|
||||
color: "#FFAA42"
|
||||
default:
|
||||
color: "#FFFFFF"
|
||||
downloaded:
|
||||
color: "#00FF00"
|
||||
downloading:
|
||||
color: "#762671"
|
||||
failure:
|
||||
color: "#DE382B"
|
||||
help:
|
||||
color: "#00FFFF"
|
||||
missing:
|
||||
color: "#DE382B"
|
||||
primary:
|
||||
color: "#2CB5E9"
|
||||
secondary:
|
||||
color: "#FFC706"
|
||||
success:
|
||||
color: "#39B54A"
|
||||
system_function:
|
||||
color: "#FFC706"
|
||||
unmonitored:
|
||||
color: "#808080"
|
||||
unmonitored_missing:
|
||||
color: "#FFC706"
|
||||
unreleased:
|
||||
color: "#00FFFF"
|
||||
warning:
|
||||
color: "#FF00FF"
|
||||
@@ -0,0 +1,9 @@
|
||||
# Managarr Dracula Theme
|
||||
The [themes.yml](./themes.yml) file in this directory corresponds to the theme configuration for the
|
||||
Dracula Managarr theme.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||
|
After Width: | Height: | Size: 378 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 257 KiB |
@@ -0,0 +1,29 @@
|
||||
- name: dracula
|
||||
theme:
|
||||
background:
|
||||
enabled: false
|
||||
color: "#282a36"
|
||||
default:
|
||||
color: "#f8f8f2"
|
||||
downloaded:
|
||||
color: "#50fa7b"
|
||||
downloading:
|
||||
color: "#8be9fd"
|
||||
failure:
|
||||
color: "#ff5555"
|
||||
missing:
|
||||
color: "#ffb86c"
|
||||
unmonitored-missing:
|
||||
color: "#6272a4"
|
||||
help:
|
||||
color: "#6272a4"
|
||||
primary:
|
||||
color: "#ff79c6"
|
||||
secondary:
|
||||
color: "#ff79c6"
|
||||
success:
|
||||
color: "#50fa7b"
|
||||
warning:
|
||||
color: "#f1fa8c"
|
||||
unreleased:
|
||||
color: "#f8f8f2"
|
||||
@@ -0,0 +1,9 @@
|
||||
# Managarr Watermelon Dark Theme
|
||||
The [themes.yml](./themes.yml) file in this directory corresponds to the theme configuration for the
|
||||
Watermelon Dark Managarr theme.
|
||||
|
||||
## Screenshots
|
||||
|
||||

|
||||

|
||||

|
||||
|
After Width: | Height: | Size: 376 KiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 255 KiB |
@@ -0,0 +1,16 @@
|
||||
- name: watermelon-dark
|
||||
theme:
|
||||
background:
|
||||
enabled: false
|
||||
default:
|
||||
color: "#00FF00"
|
||||
downloaded:
|
||||
color: "#80ffbf"
|
||||
failure:
|
||||
color: "#ff8080"
|
||||
missing:
|
||||
color: "#ff8080"
|
||||
primary:
|
||||
color: "#ff19d9"
|
||||
secondary:
|
||||
color: "#8c19ff"
|
||||