@@ -1391,6 +1391,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
|
"validate_theme_derive",
|
||||||
"veil",
|
"veil",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2780,6 +2781,15 @@ dependencies = [
|
|||||||
"getrandom 0.3.1",
|
"getrandom 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "validate_theme_derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.99",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ rust-version = "1.85.0"
|
|||||||
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
|
exclude = [".github", "CONTRIBUTING.md", "*.log", "tags"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = ["proc_macros/enum_display_style_derive"]
|
members = ["proc_macros/enum_display_style_derive", "proc_macros/validate_theme_derive"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.68"
|
anyhow = "1.0.68"
|
||||||
@@ -63,6 +63,7 @@ deunicode = "1.6.0"
|
|||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
openssl = { version = "0.10.70", features = ["vendored"] }
|
openssl = { version = "0.10.70", features = ["vendored"] }
|
||||||
veil = "0.2.0"
|
veil = "0.2.0"
|
||||||
|
validate_theme_derive = { path = "proc_macros/validate_theme_derive" }
|
||||||
enum_display_style_derive = { path = "proc_macros/enum_display_style_derive" }
|
enum_display_style_derive = { path = "proc_macros/enum_display_style_derive" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -206,6 +206,16 @@ Key:
|
|||||||
|
|
||||||
- [ ] Support for Tautulli
|
- [ ] Support for Tautulli
|
||||||
|
|
||||||
|
### Themes
|
||||||
|
Managarr ships with a few themes out of the box. Here's a few examples:
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
You can also create your own custom themes as well. To learn more about what themes are built-in to Managarr and how
|
||||||
|
to create your own custom themes, check out the [Themes README](themes/README.md).
|
||||||
|
|
||||||
### The Managarr CLI
|
### The Managarr CLI
|
||||||
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
|
Managarr can be used in one of two ways: As a TUI, or as a CLI for managing your Servarrs.
|
||||||
|
|
||||||
@@ -218,7 +228,7 @@ To see all available commands, simply run `managarr --help`:
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
$ managarr --help
|
$ managarr --help
|
||||||
managarr 0.5.0
|
managarr 0.5.1
|
||||||
Alex Clarke <alex.j.tusa@gmail.com>
|
Alex Clarke <alex.j.tusa@gmail.com>
|
||||||
|
|
||||||
A TUI and CLI to manage your Servarrs
|
A TUI and CLI to manage your Servarrs
|
||||||
@@ -235,6 +245,8 @@ Commands:
|
|||||||
Options:
|
Options:
|
||||||
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
--disable-spinner Disable the spinner (can sometimes make parsing output challenging) [env: MANAGARR_DISABLE_SPINNER=]
|
||||||
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
--config-file <CONFIG_FILE> The Managarr configuration file to use [env: MANAGARR_CONFIG_FILE=]
|
||||||
|
--themes-file <THEMES_FILE> The Managarr themes file to use [env: MANAGARR_THEMES_FILE=]
|
||||||
|
--theme <THEME> The name of the Managarr theme to use [env: MANAGARR_THEME=]
|
||||||
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
--servarr-name <SERVARR_NAME> For multi-instance configurations, you need to specify the name of the instance configuration that you want to use.
|
||||||
This is useful when you have multiple instances of the same Servarr defined in your config file.
|
This is useful when you have multiple instances of the same Servarr defined in your config file.
|
||||||
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
|
By default, if left empty, the first configured Servarr instance listed in the config file will be used.
|
||||||
@@ -315,6 +327,7 @@ managarr --config-file /path/to/config.yml
|
|||||||
|
|
||||||
### Example Configuration:
|
### Example Configuration:
|
||||||
```yaml
|
```yaml
|
||||||
|
theme: default
|
||||||
radarr:
|
radarr:
|
||||||
- host: 192.168.0.78
|
- host: 192.168.0.78
|
||||||
port: 7878
|
port: 7878
|
||||||
@@ -357,6 +370,7 @@ tautulli:
|
|||||||
|
|
||||||
### Example Multi-Instance Configuration:
|
### Example Multi-Instance Configuration:
|
||||||
```yaml
|
```yaml
|
||||||
|
theme: default
|
||||||
radarr:
|
radarr:
|
||||||
- host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1'
|
- host: 192.168.0.78 # No name specified, so this instance's name will default to 'Radarr 1'
|
||||||
port: 7878
|
port: 7878
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use darling::FromVariant;
|
|||||||
use quote::quote;
|
use quote::quote;
|
||||||
use syn::{Data, DeriveInput, parse_macro_input};
|
use syn::{Data, DeriveInput, parse_macro_input};
|
||||||
|
|
||||||
/// Derive macro for the EnumDisplayStyle trait.
|
/// Derive macro for generating a `to_display_str` method for an enum.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
@@ -22,7 +22,6 @@ use syn::{Data, DeriveInput, parse_macro_input};
|
|||||||
///
|
///
|
||||||
/// assert_eq!(Weekend::Saturday.to_display_str(), "Saturday");
|
/// assert_eq!(Weekend::Saturday.to_display_str(), "Saturday");
|
||||||
/// assert_eq!(Weekend::Sunday.to_display_str(), "Sunday");
|
/// assert_eq!(Weekend::Sunday.to_display_str(), "Sunday");
|
||||||
///
|
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// Using custom values for the display style:
|
/// Using custom values for the display style:
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "validate_theme_derive"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
quote = "1.0.39"
|
||||||
|
syn = "2.0.99"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
log = "0.4.17"
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
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`.
|
||||||
|
/// Otherwise, an error message it output to both the log file and stdout and the program exits.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// Valid themes pass through the program transitively without any messages being output.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use validate_theme_derive::ValidateTheme;
|
||||||
|
///
|
||||||
|
/// #[derive(ValidateTheme, Default)]
|
||||||
|
/// struct Theme {
|
||||||
|
/// pub name: String,
|
||||||
|
/// #[validate]
|
||||||
|
/// pub good: Option<Style>,
|
||||||
|
/// #[validate]
|
||||||
|
/// pub bad: Option<Style>,
|
||||||
|
/// pub ugly: Option<Style>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// struct Style {
|
||||||
|
/// color: String,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let theme = Theme {
|
||||||
|
/// good: Some(Style { color: "Green".to_owned() }),
|
||||||
|
/// bad: Some(Style { color: "Red".to_owned() }),
|
||||||
|
/// ..Theme::default()
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // Since only `good` and `bad` have the `validate` attribute, the `validate` method will only check those fields.
|
||||||
|
/// theme.validate();
|
||||||
|
/// // Since both `good` and `bad` have values, the program will not exit and no message is output.
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Invalid themes will output an error message to both the log file and stdout and the program will exit.
|
||||||
|
///
|
||||||
|
/// ```should_panic
|
||||||
|
/// use validate_theme_derive::ValidateTheme;
|
||||||
|
///
|
||||||
|
/// #[derive(ValidateTheme, Default)]
|
||||||
|
/// struct Theme {
|
||||||
|
/// pub name: String,
|
||||||
|
/// #[validate]
|
||||||
|
/// pub good: Option<Style>,
|
||||||
|
/// #[validate]
|
||||||
|
/// pub bad: Option<Style>,
|
||||||
|
/// pub ugly: Option<Style>,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// struct Style {
|
||||||
|
/// color: String,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let theme = Theme {
|
||||||
|
/// bad: Some(Style { color: "Red".to_owned() }),
|
||||||
|
/// ..Theme::default()
|
||||||
|
/// };
|
||||||
|
///
|
||||||
|
/// // Since `good` has the `validate` attribute and since `good` is `None`, the `validate` method will output an error message and exit the program.
|
||||||
|
/// theme.validate();
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_derive(ValidateTheme, attributes(validate))]
|
||||||
|
pub fn derive_validate_theme(input: TokenStream) -> TokenStream {
|
||||||
|
let input = parse_macro_input!(input as DeriveInput);
|
||||||
|
let struct_name = &input.ident;
|
||||||
|
|
||||||
|
let mut validation_checks = Vec::new();
|
||||||
|
|
||||||
|
if let Data::Struct(data_struct) = &input.data {
|
||||||
|
if let Fields::Named(fields) = &data_struct.fields {
|
||||||
|
for field in &fields.named {
|
||||||
|
let field_name = &field.ident;
|
||||||
|
|
||||||
|
let has_validate_attr = field
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.any(|attr| attr.path().is_ident("validate"));
|
||||||
|
|
||||||
|
if has_validate_attr {
|
||||||
|
validation_checks.push(quote! {
|
||||||
|
if self.#field_name.is_none() {
|
||||||
|
log::error!("{} is missing a color value.", stringify!(#field_name));
|
||||||
|
eprintln!("{} is missing a color value.", stringify!(#field_name));
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
impl #struct_name {
|
||||||
|
pub fn validate(&self) {
|
||||||
|
#(#validation_checks)*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 382 KiB After Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 211 KiB After Width: | Height: | Size: 241 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 220 KiB |
@@ -31,6 +31,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
let sonarr_config_2 = ServarrConfig::default();
|
let sonarr_config_2 = ServarrConfig::default();
|
||||||
let config = AppConfig {
|
let config = AppConfig {
|
||||||
|
theme: None,
|
||||||
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
radarr: Some(vec![radarr_config_1.clone(), radarr_config_2.clone()]),
|
||||||
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
sonarr: Some(vec![sonarr_config_1.clone(), sonarr_config_2.clone()]),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ pub struct Data<'a> {
|
|||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
#[derive(Debug, Deserialize, Serialize, Default, Clone)]
|
||||||
pub struct AppConfig {
|
pub struct AppConfig {
|
||||||
|
pub theme: Option<String>,
|
||||||
pub radarr: Option<Vec<ServarrConfig>>,
|
pub radarr: Option<Vec<ServarrConfig>>,
|
||||||
pub sonarr: Option<Vec<ServarrConfig>>,
|
pub sonarr: Option<Vec<ServarrConfig>>,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::panic::PanicHookInfo;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::{io, panic, process};
|
|
||||||
|
|
||||||
use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser};
|
use clap::{crate_authors, crate_description, crate_name, crate_version, CommandFactory, Parser};
|
||||||
use clap_complete::generate;
|
use clap_complete::generate;
|
||||||
use crossterm::execute;
|
use crossterm::execute;
|
||||||
@@ -16,6 +10,11 @@ use network::NetworkTrait;
|
|||||||
use ratatui::backend::CrosstermBackend;
|
use ratatui::backend::CrosstermBackend;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
use std::panic::PanicHookInfo;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::{io, panic, process};
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use tokio::sync::mpsc::Receiver;
|
use tokio::sync::mpsc::Receiver;
|
||||||
use tokio::sync::{mpsc, Mutex};
|
use tokio::sync::{mpsc, Mutex};
|
||||||
@@ -24,12 +23,14 @@ use utils::{
|
|||||||
build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs,
|
build_network_client, load_config, start_cli_no_spinner, start_cli_with_spinner, tail_logs,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::{log_and_print_error, App};
|
||||||
use crate::cli::Command;
|
use crate::cli::Command;
|
||||||
use crate::event::input_event::{Events, InputEvent};
|
use crate::event::input_event::{Events, InputEvent};
|
||||||
use crate::event::Key;
|
use crate::event::Key;
|
||||||
use crate::network::{Network, NetworkEvent};
|
use crate::network::{Network, NetworkEvent};
|
||||||
use crate::ui::ui;
|
use crate::ui::theme::{Theme, ThemeDefinitionsWrapper};
|
||||||
|
use crate::ui::{ui, THEME};
|
||||||
|
use crate::utils::load_theme_config;
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
mod cli;
|
mod cli;
|
||||||
@@ -74,6 +75,22 @@ struct Cli {
|
|||||||
help = "The Managarr configuration file to use"
|
help = "The Managarr configuration file to use"
|
||||||
)]
|
)]
|
||||||
config_file: Option<PathBuf>,
|
config_file: Option<PathBuf>,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
value_parser,
|
||||||
|
env = "MANAGARR_THEMES_FILE",
|
||||||
|
help = "The Managarr themes file to use"
|
||||||
|
)]
|
||||||
|
themes_file: Option<PathBuf>,
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
global = true,
|
||||||
|
value_parser,
|
||||||
|
env = "MANAGARR_THEME",
|
||||||
|
help = "The name of the Managarr theme to use"
|
||||||
|
)]
|
||||||
|
theme: Option<String>,
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
global = true,
|
global = true,
|
||||||
@@ -98,10 +115,12 @@ async fn main() -> Result<()> {
|
|||||||
} else {
|
} else {
|
||||||
confy::load("managarr", "config")?
|
confy::load("managarr", "config")?
|
||||||
};
|
};
|
||||||
|
let theme_name = config.theme.clone();
|
||||||
let spinner_disabled = args.disable_spinner;
|
let spinner_disabled = args.disable_spinner;
|
||||||
debug!("Managarr loaded using config: {config:?}");
|
debug!("Managarr loaded using config: {config:?}");
|
||||||
config.validate();
|
config.validate();
|
||||||
config.post_process_initialization();
|
config.post_process_initialization();
|
||||||
|
|
||||||
let reqwest_client = build_network_client(&config);
|
let reqwest_client = build_network_client(&config);
|
||||||
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
|
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
|
||||||
let cancellation_token = CancellationToken::new();
|
let cancellation_token = CancellationToken::new();
|
||||||
@@ -140,7 +159,12 @@ async fn main() -> Result<()> {
|
|||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
|
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
|
||||||
});
|
});
|
||||||
start_ui(&app).await?;
|
start_ui(
|
||||||
|
&app,
|
||||||
|
&args.themes_file,
|
||||||
|
args.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +198,36 @@ async fn start_networking(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
|
async fn start_ui(
|
||||||
|
app: &Arc<Mutex<App<'_>>>,
|
||||||
|
themes_file_arg: &Option<PathBuf>,
|
||||||
|
theme_name: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
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(|_| ThemeDefinitionsWrapper::default())
|
||||||
|
};
|
||||||
|
let theme = if !theme_name.is_empty() {
|
||||||
|
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}"));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
theme_definition.unwrap().theme
|
||||||
|
} else {
|
||||||
|
debug!("No theme specified, using default theme");
|
||||||
|
Theme::default()
|
||||||
|
};
|
||||||
|
debug!("Managarr loaded using theme: {theme:?}");
|
||||||
|
theme.validate();
|
||||||
|
THEME.set(theme);
|
||||||
|
|
||||||
let mut stdout = io::stdout();
|
let mut stdout = io::stdout();
|
||||||
enable_raw_mode()?;
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,148 @@
|
|||||||
|
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(in crate::ui) fn watermelon_dark_theme() -> Theme {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui) fn dracula_theme() -> Theme {
|
||||||
|
Theme {
|
||||||
|
background: Some(Background {
|
||||||
|
enabled: Some(true),
|
||||||
|
color: Some(Color::from_str("#232326").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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(in crate::ui) fn eldritch_theme() -> Theme {
|
||||||
|
Theme {
|
||||||
|
background: Some(Background {
|
||||||
|
enabled: Some(true),
|
||||||
|
color: Some(Color::from_str("#212337").unwrap()),
|
||||||
|
}),
|
||||||
|
default: Some(Style {
|
||||||
|
color: Some(Color::from_str("#ebfafa").unwrap()),
|
||||||
|
}),
|
||||||
|
downloaded: Some(Style {
|
||||||
|
color: Some(Color::from_str("#37f499").unwrap()),
|
||||||
|
}),
|
||||||
|
downloading: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f7c67f").unwrap()),
|
||||||
|
}),
|
||||||
|
failure: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f16c75").unwrap()),
|
||||||
|
}),
|
||||||
|
missing: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f7c67f").unwrap()),
|
||||||
|
}),
|
||||||
|
unmonitored_missing: Some(Style {
|
||||||
|
color: Some(Color::from_str("#7081d0").unwrap()),
|
||||||
|
}),
|
||||||
|
help: Some(Style {
|
||||||
|
color: Some(Color::from_str("#7081d0").unwrap()),
|
||||||
|
}),
|
||||||
|
primary: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f265b5").unwrap()),
|
||||||
|
}),
|
||||||
|
secondary: Some(Style {
|
||||||
|
color: Some(Color::from_str("#04d1f9").unwrap()),
|
||||||
|
}),
|
||||||
|
success: Some(Style {
|
||||||
|
color: Some(Color::from_str("#37f499").unwrap()),
|
||||||
|
}),
|
||||||
|
warning: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f1fc79").unwrap()),
|
||||||
|
}),
|
||||||
|
unreleased: Some(Style {
|
||||||
|
color: Some(Color::from_str("#ebfafa").unwrap()),
|
||||||
|
}),
|
||||||
|
..Theme::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_builtin_themes() -> Vec<ThemeDefinition> {
|
||||||
|
vec![
|
||||||
|
ThemeDefinition {
|
||||||
|
name: "default".to_owned(),
|
||||||
|
theme: Theme::default(),
|
||||||
|
},
|
||||||
|
ThemeDefinition {
|
||||||
|
name: "watermelon-dark".to_owned(),
|
||||||
|
theme: watermelon_dark_theme(),
|
||||||
|
},
|
||||||
|
ThemeDefinition {
|
||||||
|
name: "dracula".to_owned(),
|
||||||
|
theme: dracula_theme(),
|
||||||
|
},
|
||||||
|
ThemeDefinition {
|
||||||
|
name: "eldritch".to_owned(),
|
||||||
|
theme: eldritch_theme(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use crate::ui::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(true),
|
||||||
|
color: Some(Color::from_str("#232326").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 eldritch = Theme {
|
||||||
|
background: Some(Background {
|
||||||
|
enabled: Some(true),
|
||||||
|
color: Some(Color::from_str("#212337").unwrap()),
|
||||||
|
}),
|
||||||
|
default: Some(Style {
|
||||||
|
color: Some(Color::from_str("#ebfafa").unwrap()),
|
||||||
|
}),
|
||||||
|
downloaded: Some(Style {
|
||||||
|
color: Some(Color::from_str("#37f499").unwrap()),
|
||||||
|
}),
|
||||||
|
downloading: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f7c67f").unwrap()),
|
||||||
|
}),
|
||||||
|
failure: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f16c75").unwrap()),
|
||||||
|
}),
|
||||||
|
missing: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f7c67f").unwrap()),
|
||||||
|
}),
|
||||||
|
unmonitored_missing: Some(Style {
|
||||||
|
color: Some(Color::from_str("#7081d0").unwrap()),
|
||||||
|
}),
|
||||||
|
help: Some(Style {
|
||||||
|
color: Some(Color::from_str("#7081d0").unwrap()),
|
||||||
|
}),
|
||||||
|
primary: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f265b5").unwrap()),
|
||||||
|
}),
|
||||||
|
secondary: Some(Style {
|
||||||
|
color: Some(Color::from_str("#04d1f9").unwrap()),
|
||||||
|
}),
|
||||||
|
success: Some(Style {
|
||||||
|
color: Some(Color::from_str("#37f499").unwrap()),
|
||||||
|
}),
|
||||||
|
warning: Some(Style {
|
||||||
|
color: Some(Color::from_str("#f1fc79").unwrap()),
|
||||||
|
}),
|
||||||
|
unreleased: Some(Style {
|
||||||
|
color: Some(Color::from_str("#ebfafa").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,
|
||||||
|
},
|
||||||
|
ThemeDefinition {
|
||||||
|
name: "eldritch".to_owned(),
|
||||||
|
theme: eldritch,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
assert_eq!(expected_themes, get_builtin_themes());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::cell::Cell;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||||
@@ -15,19 +16,26 @@ use crate::app::App;
|
|||||||
use crate::models::{HorizontallyScrollableText, Route, TabState};
|
use crate::models::{HorizontallyScrollableText, Route, TabState};
|
||||||
use crate::ui::radarr_ui::RadarrUi;
|
use crate::ui::radarr_ui::RadarrUi;
|
||||||
use crate::ui::styles::ManagarrStyle;
|
use crate::ui::styles::ManagarrStyle;
|
||||||
|
use crate::ui::theme::Theme;
|
||||||
use crate::ui::utils::{
|
use crate::ui::utils::{
|
||||||
background_block, borderless_block, centered_rect, logo_block, title_block, title_block_centered,
|
background_block, borderless_block, centered_rect, logo_block, title_block, title_block_centered,
|
||||||
|
unstyled_title_block,
|
||||||
};
|
};
|
||||||
use crate::ui::widgets::input_box::InputBox;
|
use crate::ui::widgets::input_box::InputBox;
|
||||||
use crate::ui::widgets::popup::Size;
|
use crate::ui::widgets::popup::Size;
|
||||||
|
|
||||||
|
mod builtin_themes;
|
||||||
mod radarr_ui;
|
mod radarr_ui;
|
||||||
mod sonarr_ui;
|
mod sonarr_ui;
|
||||||
mod styles;
|
mod styles;
|
||||||
|
pub mod theme;
|
||||||
mod utils;
|
mod utils;
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
static HIGHLIGHT_SYMBOL: &str = "=> ";
|
static HIGHLIGHT_SYMBOL: &str = "=> ";
|
||||||
|
thread_local! {
|
||||||
|
pub static THEME: Cell<Theme> = Cell::new(Theme::default());
|
||||||
|
}
|
||||||
|
|
||||||
pub trait DrawUi {
|
pub trait DrawUi {
|
||||||
fn accepts(route: Route) -> bool;
|
fn accepts(route: Route) -> bool;
|
||||||
@@ -100,7 +108,9 @@ fn draw_header_row(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn draw_error(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw_error(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
let block = title_block("Error | <esc> to close").failure().bold();
|
let block = unstyled_title_block("Error | <esc> to close")
|
||||||
|
.failure()
|
||||||
|
.bold();
|
||||||
|
|
||||||
app.error.scroll_left_or_reset(
|
app.error.scroll_left_or_reset(
|
||||||
area.width as usize,
|
area.width as usize,
|
||||||
@@ -130,7 +140,7 @@ pub fn draw_popup(
|
|||||||
|
|
||||||
fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect {
|
fn draw_tabs(f: &mut Frame<'_>, area: Rect, title: &str, tab_state: &TabState) -> Rect {
|
||||||
if title.is_empty() {
|
if title.is_empty() {
|
||||||
f.render_widget(layout_block(), area);
|
f.render_widget(layout_block().default(), area);
|
||||||
} else {
|
} else {
|
||||||
f.render_widget(title_block(title), area);
|
f.render_widget(title_block(title), area);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ impl DrawUi for CollectionDetailsUi {
|
|||||||
if let Route::Radarr(active_radarr_block, context_option) = route {
|
if let Route::Radarr(active_radarr_block, context_option) = route {
|
||||||
if let Some(context) = context_option {
|
if let Some(context) = context_option {
|
||||||
return COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block)
|
return COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block)
|
||||||
&& context == ActiveRadarrBlock::CollectionDetails;
|
|| context == ActiveRadarrBlock::CollectionDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
return COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block);
|
return COLLECTION_DETAILS_BLOCKS.contains(&active_radarr_block);
|
||||||
|
|||||||
@@ -25,5 +25,12 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
));
|
));
|
||||||
|
assert!(CollectionDetailsUi::accepts(
|
||||||
|
(
|
||||||
|
ActiveRadarrBlock::AddMoviePrompt,
|
||||||
|
Some(ActiveRadarrBlock::CollectionDetails)
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let title = format!("Edit - {collection_title}");
|
let title = format!("Edit - {collection_title}");
|
||||||
|
f.render_widget(title_block_centered(&title), area);
|
||||||
let yes_no_value = app.data.radarr_data.prompt_confirm;
|
let yes_no_value = app.data.radarr_data.prompt_confirm;
|
||||||
let selected_block = app.data.radarr_data.selected_block.get_active_block();
|
let selected_block = app.data.radarr_data.selected_block.get_active_block();
|
||||||
let highlight_yes_no = selected_block == ActiveRadarrBlock::EditCollectionConfirmPrompt;
|
let highlight_yes_no = selected_block == ActiveRadarrBlock::EditCollectionConfirmPrompt;
|
||||||
@@ -155,7 +156,6 @@ fn draw_edit_collection_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>
|
|||||||
.title("Cancel")
|
.title("Cancel")
|
||||||
.selected(!yes_no_value && highlight_yes_no);
|
.selected(!yes_no_value && highlight_yes_no);
|
||||||
|
|
||||||
f.render_widget(title_block_centered(&title), area);
|
|
||||||
f.render_widget(prompt_paragraph, paragraph_area);
|
f.render_widget(prompt_paragraph, paragraph_area);
|
||||||
f.render_widget(monitored_checkbox, monitored_area);
|
f.render_widget(monitored_checkbox, monitored_area);
|
||||||
f.render_widget(min_availability_drop_down_button, min_availability_area);
|
f.render_widget(min_availability_drop_down_button, min_availability_area);
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
let help_paragraph = Paragraph::new(help_text).centered();
|
let help_paragraph = Paragraph::new(help_text).centered();
|
||||||
|
|
||||||
if edit_indexer_modal_option.is_some() {
|
if edit_indexer_modal_option.is_some() {
|
||||||
|
f.render_widget(block, area);
|
||||||
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
|
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
|
||||||
|
|
||||||
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([
|
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([
|
||||||
@@ -163,7 +164,6 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.title("Cancel")
|
.title("Cancel")
|
||||||
.selected(!yes_no_value && highlight_yes_no);
|
.selected(!yes_no_value && highlight_yes_no);
|
||||||
|
|
||||||
f.render_widget(block, area);
|
|
||||||
f.render_widget(rss_checkbox, rss_area);
|
f.render_widget(rss_checkbox, rss_area);
|
||||||
f.render_widget(auto_search_checkbox, auto_search_area);
|
f.render_widget(auto_search_checkbox, auto_search_area);
|
||||||
f.render_widget(interactive_search_checkbox, interactive_search_area);
|
f.render_widget(interactive_search_checkbox, interactive_search_area);
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
|
|||||||
let help_paragraph = Paragraph::new(help_text).centered();
|
let help_paragraph = Paragraph::new(help_text).centered();
|
||||||
|
|
||||||
if indexer_settings_option.is_some() {
|
if indexer_settings_option.is_some() {
|
||||||
|
f.render_widget(block, area);
|
||||||
let indexer_settings = indexer_settings_option.as_ref().unwrap();
|
let indexer_settings = indexer_settings_option.as_ref().unwrap();
|
||||||
|
|
||||||
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([
|
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([
|
||||||
@@ -162,7 +163,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
|
|||||||
.title("Cancel")
|
.title("Cancel")
|
||||||
.selected(!yes_no_value && highlight_yes_no);
|
.selected(!yes_no_value && highlight_yes_no);
|
||||||
|
|
||||||
f.render_widget(block, area);
|
|
||||||
f.render_widget(prefer_indexer_flags_checkbox, prefer_flags_area);
|
f.render_widget(prefer_indexer_flags_checkbox, prefer_flags_area);
|
||||||
f.render_widget(allow_hardcoded_subs_checkbox, allow_hardcoded_subs_area);
|
f.render_widget(allow_hardcoded_subs_checkbox, allow_hardcoded_subs_area);
|
||||||
f.render_widget(save_button, save_area);
|
f.render_widget(save_button, save_area);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
|||||||
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
use crate::models::servarr_data::radarr::radarr_data::ActiveRadarrBlock;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::ui::styles::ManagarrStyle;
|
use crate::ui::styles::ManagarrStyle;
|
||||||
use crate::ui::utils::{borderless_block, get_width_from_percentage, title_block};
|
use crate::ui::utils::{get_width_from_percentage, title_block};
|
||||||
use crate::ui::widgets::managarr_table::ManagarrTable;
|
use crate::ui::widgets::managarr_table::ManagarrTable;
|
||||||
use crate::ui::widgets::popup::Size;
|
use crate::ui::widgets::popup::Size;
|
||||||
use crate::ui::{draw_popup, DrawUi};
|
use crate::ui::{draw_popup, DrawUi};
|
||||||
@@ -71,7 +71,6 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are
|
|||||||
app.data.radarr_data.indexer_test_all_results.as_mut(),
|
app.data.radarr_data.indexer_test_all_results.as_mut(),
|
||||||
test_results_row_mapping,
|
test_results_row_mapping,
|
||||||
)
|
)
|
||||||
.block(borderless_block())
|
|
||||||
.loading(is_loading)
|
.loading(is_loading)
|
||||||
.footer(Some(help_footer))
|
.footer(Some(help_footer))
|
||||||
.footer_alignment(Alignment::Center)
|
.footer_alignment(Alignment::Center)
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.centered();
|
.centered();
|
||||||
|
|
||||||
search_box.show_cursor(f, search_box_area);
|
search_box.show_cursor(f, search_box_area);
|
||||||
f.render_widget(layout_block(), results_area);
|
f.render_widget(layout_block().default(), results_area);
|
||||||
f.render_widget(search_box, search_box_area);
|
f.render_widget(search_box, search_box_area);
|
||||||
f.render_widget(help_paragraph, help_area);
|
f.render_widget(help_paragraph, help_area);
|
||||||
}
|
}
|
||||||
@@ -182,7 +182,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
let error_message = Message::new("No movies found matching your query!");
|
let error_message = Message::new("No movies found matching your query!");
|
||||||
let error_message_popup = Popup::new(error_message).size(Size::Message);
|
let error_message_popup = Popup::new(error_message).size(Size::Message);
|
||||||
|
|
||||||
f.render_widget(layout_block(), results_area);
|
f.render_widget(layout_block().default(), results_area);
|
||||||
f.render_widget(error_message_popup, f.area());
|
f.render_widget(error_message_popup, f.area());
|
||||||
f.render_widget(help_paragraph, help_area);
|
f.render_widget(help_paragraph, help_area);
|
||||||
}
|
}
|
||||||
@@ -204,7 +204,7 @@ fn draw_add_movie_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
search_results_row_mapping,
|
search_results_row_mapping,
|
||||||
)
|
)
|
||||||
.loading(is_loading)
|
.loading(is_loading)
|
||||||
.block(layout_block())
|
.block(layout_block().default())
|
||||||
.headers([
|
.headers([
|
||||||
"✔",
|
"✔",
|
||||||
"Title",
|
"Title",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
|
|||||||
.overview
|
.overview
|
||||||
.clone();
|
.clone();
|
||||||
let title = format!("Edit - {movie_title}");
|
let title = format!("Edit - {movie_title}");
|
||||||
|
f.render_widget(title_block_centered(&title), area);
|
||||||
let yes_no_value = app.data.radarr_data.prompt_confirm;
|
let yes_no_value = app.data.radarr_data.prompt_confirm;
|
||||||
let selected_block = app.data.radarr_data.selected_block.get_active_block();
|
let selected_block = app.data.radarr_data.selected_block.get_active_block();
|
||||||
let highlight_yes_no = selected_block == ActiveRadarrBlock::EditMovieConfirmPrompt;
|
let highlight_yes_no = selected_block == ActiveRadarrBlock::EditMovieConfirmPrompt;
|
||||||
@@ -157,7 +158,6 @@ fn draw_edit_movie_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, are
|
|||||||
.title("Cancel")
|
.title("Cancel")
|
||||||
.selected(!yes_no_value && highlight_yes_no);
|
.selected(!yes_no_value && highlight_yes_no);
|
||||||
|
|
||||||
f.render_widget(title_block_centered(&title), area);
|
|
||||||
f.render_widget(prompt_paragraph, paragraph_area);
|
f.render_widget(prompt_paragraph, paragraph_area);
|
||||||
f.render_widget(monitored_checkbox, monitored_area);
|
f.render_widget(monitored_checkbox, monitored_area);
|
||||||
f.render_widget(min_availability_drop_down_button, min_availability_area);
|
f.render_widget(min_availability_drop_down_button, min_availability_area);
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ impl DrawUi for LibraryUi {
|
|||||||
|
|
||||||
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
let route = app.get_current_route();
|
let route = app.get_current_route();
|
||||||
|
if let Route::Radarr(_, context_option) = route {
|
||||||
|
if context_option.is_some() && AddMovieUi::accepts(route) {
|
||||||
|
AddMovieUi::draw(f, app, area);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
draw_library(f, app, area);
|
draw_library(f, app, area);
|
||||||
|
|
||||||
match route {
|
match route {
|
||||||
@@ -66,6 +72,7 @@ impl DrawUi for LibraryUi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
fn draw_library(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
||||||
if let Route::Radarr(active_radarr_block, _) = app.get_current_route() {
|
if let Route::Radarr(active_radarr_block, _) = app.get_current_route() {
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ fn draw_movie_history(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(quality.quality.name.to_owned()),
|
Cell::from(quality.quality.name.to_owned()),
|
||||||
Cell::from(date.to_string()),
|
Cell::from(date.to_string()),
|
||||||
])
|
])
|
||||||
.success()
|
.primary()
|
||||||
};
|
};
|
||||||
let help_footer = app
|
let help_footer = app
|
||||||
.data
|
.data
|
||||||
@@ -297,7 +297,7 @@ fn draw_movie_cast(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(person_name.to_owned()),
|
Cell::from(person_name.to_owned()),
|
||||||
Cell::from(character.clone().unwrap_or_default()),
|
Cell::from(character.clone().unwrap_or_default()),
|
||||||
])
|
])
|
||||||
.success()
|
.primary()
|
||||||
};
|
};
|
||||||
let content = Some(&mut movie_details_modal.movie_cast);
|
let content = Some(&mut movie_details_modal.movie_cast);
|
||||||
let help_footer = app
|
let help_footer = app
|
||||||
@@ -340,7 +340,7 @@ fn draw_movie_crew(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
Cell::from(job.clone().unwrap_or_default()),
|
Cell::from(job.clone().unwrap_or_default()),
|
||||||
Cell::from(department.clone().unwrap_or_default()),
|
Cell::from(department.clone().unwrap_or_default()),
|
||||||
])
|
])
|
||||||
.success()
|
.primary()
|
||||||
};
|
};
|
||||||
let content = Some(&mut movie_details_modal.movie_crew);
|
let content = Some(&mut movie_details_modal.movie_crew);
|
||||||
let help_footer = app
|
let help_footer = app
|
||||||
|
|||||||
@@ -105,7 +105,6 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.primary()
|
.primary()
|
||||||
};
|
};
|
||||||
let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping)
|
let tasks_table = ManagarrTable::new(Some(&mut app.data.radarr_data.tasks), tasks_row_mapping)
|
||||||
.block(borderless_block())
|
|
||||||
.loading(app.is_loading)
|
.loading(app.is_loading)
|
||||||
.margin(1)
|
.margin(1)
|
||||||
.footer(help_footer)
|
.footer(help_footer)
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
let help_paragraph = Paragraph::new(help_text).centered();
|
let help_paragraph = Paragraph::new(help_text).centered();
|
||||||
|
|
||||||
if edit_indexer_modal_option.is_some() {
|
if edit_indexer_modal_option.is_some() {
|
||||||
|
f.render_widget(block, area);
|
||||||
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
|
let edit_indexer_modal = edit_indexer_modal_option.as_ref().unwrap();
|
||||||
|
|
||||||
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([
|
let [_, settings_area, _, buttons_area, help_area] = Layout::vertical([
|
||||||
@@ -163,7 +164,6 @@ fn draw_edit_indexer_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.title("Cancel")
|
.title("Cancel")
|
||||||
.selected(!yes_no_value && highlight_yes_no);
|
.selected(!yes_no_value && highlight_yes_no);
|
||||||
|
|
||||||
f.render_widget(block, area);
|
|
||||||
f.render_widget(rss_checkbox, rss_area);
|
f.render_widget(rss_checkbox, rss_area);
|
||||||
f.render_widget(auto_search_checkbox, auto_search_area);
|
f.render_widget(auto_search_checkbox, auto_search_area);
|
||||||
f.render_widget(interactive_search_checkbox, interactive_search_area);
|
f.render_widget(interactive_search_checkbox, interactive_search_area);
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
|
|||||||
let help_paragraph = Paragraph::new(help_text).centered();
|
let help_paragraph = Paragraph::new(help_text).centered();
|
||||||
|
|
||||||
if indexer_settings_option.is_some() {
|
if indexer_settings_option.is_some() {
|
||||||
|
f.render_widget(block, area);
|
||||||
let indexer_settings = indexer_settings_option.as_ref().unwrap();
|
let indexer_settings = indexer_settings_option.as_ref().unwrap();
|
||||||
|
|
||||||
let [_, min_age_area, retention_area, max_size_area, rss_sync_area, _, buttons_area, help_area] =
|
let [_, min_age_area, retention_area, max_size_area, rss_sync_area, _, buttons_area, help_area] =
|
||||||
@@ -109,7 +110,6 @@ fn draw_edit_indexer_settings_prompt(f: &mut Frame<'_>, app: &mut App<'_>, area:
|
|||||||
.title("Cancel")
|
.title("Cancel")
|
||||||
.selected(!yes_no_value && highlight_yes_no);
|
.selected(!yes_no_value && highlight_yes_no);
|
||||||
|
|
||||||
f.render_widget(block, area);
|
|
||||||
f.render_widget(save_button, save_area);
|
f.render_widget(save_button, save_area);
|
||||||
f.render_widget(cancel_button, cancel_area);
|
f.render_widget(cancel_button, cancel_area);
|
||||||
f.render_widget(help_paragraph, help_area);
|
f.render_widget(help_paragraph, help_area);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use crate::models::servarr_data::modals::IndexerTestResultModalItem;
|
|||||||
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
use crate::models::servarr_data::sonarr::sonarr_data::ActiveSonarrBlock;
|
||||||
use crate::models::Route;
|
use crate::models::Route;
|
||||||
use crate::ui::styles::ManagarrStyle;
|
use crate::ui::styles::ManagarrStyle;
|
||||||
use crate::ui::utils::{borderless_block, get_width_from_percentage, title_block};
|
use crate::ui::utils::{get_width_from_percentage, title_block};
|
||||||
use crate::ui::widgets::managarr_table::ManagarrTable;
|
use crate::ui::widgets::managarr_table::ManagarrTable;
|
||||||
use crate::ui::widgets::popup::Size;
|
use crate::ui::widgets::popup::Size;
|
||||||
use crate::ui::{draw_popup, DrawUi};
|
use crate::ui::{draw_popup, DrawUi};
|
||||||
@@ -69,7 +69,6 @@ fn draw_test_all_indexers_test_results(f: &mut Frame<'_>, app: &mut App<'_>, are
|
|||||||
app.data.sonarr_data.indexer_test_all_results.as_mut(),
|
app.data.sonarr_data.indexer_test_all_results.as_mut(),
|
||||||
test_results_row_mapping,
|
test_results_row_mapping,
|
||||||
)
|
)
|
||||||
.block(borderless_block())
|
|
||||||
.loading(is_loading)
|
.loading(is_loading)
|
||||||
.footer(Some(help_footer))
|
.footer(Some(help_footer))
|
||||||
.footer_alignment(Alignment::Center)
|
.footer_alignment(Alignment::Center)
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.centered();
|
.centered();
|
||||||
|
|
||||||
search_box.show_cursor(f, search_box_area);
|
search_box.show_cursor(f, search_box_area);
|
||||||
f.render_widget(layout_block(), results_area);
|
f.render_widget(layout_block().default(), results_area);
|
||||||
f.render_widget(search_box, search_box_area);
|
f.render_widget(search_box, search_box_area);
|
||||||
f.render_widget(help_paragraph, help_area);
|
f.render_widget(help_paragraph, help_area);
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
let error_message = Message::new("No series found matching your query!");
|
let error_message = Message::new("No series found matching your query!");
|
||||||
let error_message_popup = Popup::new(error_message).size(Size::Message);
|
let error_message_popup = Popup::new(error_message).size(Size::Message);
|
||||||
|
|
||||||
f.render_widget(layout_block(), results_area);
|
f.render_widget(layout_block().default(), results_area);
|
||||||
f.render_widget(error_message_popup, f.area());
|
f.render_widget(error_message_popup, f.area());
|
||||||
f.render_widget(help_paragraph, help_area);
|
f.render_widget(help_paragraph, help_area);
|
||||||
}
|
}
|
||||||
@@ -191,7 +191,7 @@ fn draw_add_series_search(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
search_results_row_mapping,
|
search_results_row_mapping,
|
||||||
)
|
)
|
||||||
.loading(is_loading)
|
.loading(is_loading)
|
||||||
.block(layout_block())
|
.block(layout_block().default())
|
||||||
.headers([
|
.headers([
|
||||||
"✔", "Title", "Year", "Network", "Seasons", "Rating", "Genres",
|
"✔", "Title", "Year", "Network", "Seasons", "Rating", "Genres",
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -89,6 +89,8 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let title = format!("Edit - {series_title}");
|
let title = format!("Edit - {series_title}");
|
||||||
|
f.render_widget(title_block_centered(&title), area);
|
||||||
|
|
||||||
let yes_no_value = app.data.sonarr_data.prompt_confirm;
|
let yes_no_value = app.data.sonarr_data.prompt_confirm;
|
||||||
let selected_block = app.data.sonarr_data.selected_block.get_active_block();
|
let selected_block = app.data.sonarr_data.selected_block.get_active_block();
|
||||||
let highlight_yes_no = selected_block == ActiveSonarrBlock::EditSeriesConfirmPrompt;
|
let highlight_yes_no = selected_block == ActiveSonarrBlock::EditSeriesConfirmPrompt;
|
||||||
@@ -179,7 +181,6 @@ fn draw_edit_series_confirmation_prompt(f: &mut Frame<'_>, app: &mut App<'_>, ar
|
|||||||
.title("Cancel")
|
.title("Cancel")
|
||||||
.selected(!yes_no_value && highlight_yes_no);
|
.selected(!yes_no_value && highlight_yes_no);
|
||||||
|
|
||||||
f.render_widget(title_block_centered(&title), area);
|
|
||||||
f.render_widget(prompt_paragraph, paragraph_area);
|
f.render_widget(prompt_paragraph, paragraph_area);
|
||||||
f.render_widget(monitored_checkbox, monitored_area);
|
f.render_widget(monitored_checkbox, monitored_area);
|
||||||
f.render_widget(season_folder_checkbox, season_folder_area);
|
f.render_widget(season_folder_checkbox, season_folder_area);
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ fn draw_tasks_popup(f: &mut Frame<'_>, app: &mut App<'_>, area: Rect) {
|
|||||||
.primary()
|
.primary()
|
||||||
};
|
};
|
||||||
let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping)
|
let tasks_table = ManagarrTable::new(Some(&mut app.data.sonarr_data.tasks), tasks_row_mapping)
|
||||||
.block(borderless_block())
|
|
||||||
.loading(app.is_loading)
|
.loading(app.is_loading)
|
||||||
.margin(1)
|
.margin(1)
|
||||||
.footer(help_footer)
|
.footer(help_footer)
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
use ratatui::prelude::Color;
|
use crate::ui::THEME;
|
||||||
use ratatui::style::{Styled, Stylize};
|
use ratatui::style::{Styled, Stylize};
|
||||||
|
|
||||||
pub const COLOR_ORANGE: Color = Color::Rgb(255, 170, 66);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "styles_tests.rs"]
|
#[path = "styles_tests.rs"]
|
||||||
mod styles_tests;
|
mod styles_tests;
|
||||||
@@ -42,31 +40,31 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn awaiting_import(self) -> T {
|
fn awaiting_import(self) -> T {
|
||||||
self.fg(COLOR_ORANGE)
|
THEME.with(|theme| self.fg(theme.get().awaiting_import.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn indeterminate(self) -> T {
|
fn indeterminate(self) -> T {
|
||||||
self.fg(COLOR_ORANGE)
|
THEME.with(|theme| self.fg(theme.get().indeterminate.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default(self) -> T {
|
fn default(self) -> T {
|
||||||
self.white()
|
THEME.with(|theme| self.fg(theme.get().default.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn downloaded(self) -> T {
|
fn downloaded(self) -> T {
|
||||||
self.green()
|
THEME.with(|theme| self.fg(theme.get().downloaded.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn downloading(self) -> T {
|
fn downloading(self) -> T {
|
||||||
self.magenta()
|
THEME.with(|theme| self.fg(theme.get().downloading.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn failure(self) -> T {
|
fn failure(self) -> T {
|
||||||
self.red()
|
THEME.with(|theme| self.fg(theme.get().failure.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn help(self) -> T {
|
fn help(self) -> T {
|
||||||
self.light_blue()
|
THEME.with(|theme| self.fg(theme.get().help.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight(self) -> T {
|
fn highlight(self) -> T {
|
||||||
@@ -74,38 +72,38 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn missing(self) -> T {
|
fn missing(self) -> T {
|
||||||
self.red()
|
THEME.with(|theme| self.fg(theme.get().missing.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn primary(self) -> T {
|
fn primary(self) -> T {
|
||||||
self.cyan()
|
THEME.with(|theme| self.fg(theme.get().primary.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn secondary(self) -> T {
|
fn secondary(self) -> T {
|
||||||
self.yellow()
|
THEME.with(|theme| self.fg(theme.get().secondary.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn success(self) -> T {
|
fn success(self) -> T {
|
||||||
self.green()
|
THEME.with(|theme| self.fg(theme.get().success.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn system_function(self) -> T {
|
fn system_function(self) -> T {
|
||||||
self.yellow()
|
THEME.with(|theme| self.fg(theme.get().system_function.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unmonitored(self) -> T {
|
fn unmonitored(self) -> T {
|
||||||
self.gray()
|
THEME.with(|theme| self.fg(theme.get().unmonitored.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unmonitored_missing(self) -> T {
|
fn unmonitored_missing(self) -> T {
|
||||||
self.yellow()
|
THEME.with(|theme| self.fg(theme.get().unmonitored_missing.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unreleased(self) -> T {
|
fn unreleased(self) -> T {
|
||||||
self.light_cyan()
|
THEME.with(|theme| self.fg(theme.get().unreleased.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn warning(self) -> T {
|
fn warning(self) -> T {
|
||||||
self.magenta()
|
THEME.with(|theme| self.fg(theme.get().warning.unwrap().color.unwrap()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::ui::styles::{ManagarrStyle, COLOR_ORANGE};
|
use crate::ui::styles::ManagarrStyle;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use ratatui::prelude::Modifier;
|
use ratatui::prelude::Modifier;
|
||||||
use ratatui::style::{Style, Stylize};
|
use ratatui::style::{Color, Style, Stylize};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_new() {
|
fn test_new() {
|
||||||
@@ -14,13 +14,16 @@ mod test {
|
|||||||
fn test_style_awaiting_import() {
|
fn test_style_awaiting_import() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
Style::new().awaiting_import(),
|
Style::new().awaiting_import(),
|
||||||
Style::new().fg(COLOR_ORANGE)
|
Style::new().fg(Color::Rgb(255, 170, 66))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_style_indeterminate() {
|
fn test_style_indeterminate() {
|
||||||
assert_eq!(Style::new().indeterminate(), Style::new().fg(COLOR_ORANGE));
|
assert_eq!(
|
||||||
|
Style::new().indeterminate(),
|
||||||
|
Style::new().fg(Color::Rgb(255, 170, 66))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -0,0 +1,284 @@
|
|||||||
|
use crate::ui::builtin_themes::get_builtin_themes;
|
||||||
|
use anyhow::Result;
|
||||||
|
use derivative::Derivative;
|
||||||
|
use ratatui::style::Color;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use std::str::FromStr;
|
||||||
|
use validate_theme_derive::ValidateTheme;
|
||||||
|
|
||||||
|
#[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, ValidateTheme)]
|
||||||
|
#[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>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_awaiting_import_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Rgb(255, 170, 66)) })"))]
|
||||||
|
pub awaiting_import: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_indeterminate_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Rgb(255, 170, 66)) })"))]
|
||||||
|
pub indeterminate: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_default_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::White) })"))]
|
||||||
|
pub default: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_downloaded_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Green) })"))]
|
||||||
|
pub downloaded: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_downloading_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Magenta) })"))]
|
||||||
|
pub downloading: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_failure_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Red) })"))]
|
||||||
|
pub failure: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_help_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::LightBlue) })"))]
|
||||||
|
pub help: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_missing_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Red) })"))]
|
||||||
|
pub missing: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_primary_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Cyan) })"))]
|
||||||
|
pub primary: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_secondary_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
|
||||||
|
pub secondary: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_success_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Green) })"))]
|
||||||
|
pub success: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_system_function_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
|
||||||
|
pub system_function: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_unmonitored_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Gray) })"))]
|
||||||
|
pub unmonitored: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_unmonitored_missing_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
|
||||||
|
pub unmonitored_missing: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[serde(default = "default_unreleased_style")]
|
||||||
|
#[derivative(Default(value = "Some(Style { color: Some(Color::LightCyan) })"))]
|
||||||
|
pub unreleased: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 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())
|
||||||
|
}
|
||||||
@@ -0,0 +1,376 @@
|
|||||||
|
mod tests {
|
||||||
|
use crate::ui::builtin_themes::{dracula_theme, eldritch_theme, watermelon_dark_theme};
|
||||||
|
use crate::ui::theme::{Background, Style, Theme, ThemeDefinition, ThemeDefinitionsWrapper};
|
||||||
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
|
use ratatui::style::Color;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_background_default() {
|
||||||
|
let expected_background = Background {
|
||||||
|
enabled: Some(true),
|
||||||
|
color: Some(Color::Rgb(35, 50, 55)),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(Background::default(), expected_background);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_theme_default() {
|
||||||
|
let expected_theme = Theme {
|
||||||
|
background: Some(Background {
|
||||||
|
enabled: Some(true),
|
||||||
|
color: Some(Color::Rgb(35, 50, 55)),
|
||||||
|
}),
|
||||||
|
awaiting_import: Some(Style {
|
||||||
|
color: Some(Color::Rgb(255, 170, 66)),
|
||||||
|
}),
|
||||||
|
indeterminate: Some(Style {
|
||||||
|
color: Some(Color::Rgb(255, 170, 66)),
|
||||||
|
}),
|
||||||
|
default: Some(Style {
|
||||||
|
color: Some(Color::White),
|
||||||
|
}),
|
||||||
|
downloaded: Some(Style {
|
||||||
|
color: Some(Color::Green),
|
||||||
|
}),
|
||||||
|
downloading: Some(Style {
|
||||||
|
color: Some(Color::Magenta),
|
||||||
|
}),
|
||||||
|
failure: Some(Style {
|
||||||
|
color: Some(Color::Red),
|
||||||
|
}),
|
||||||
|
help: Some(Style {
|
||||||
|
color: Some(Color::LightBlue),
|
||||||
|
}),
|
||||||
|
missing: Some(Style {
|
||||||
|
color: Some(Color::Red),
|
||||||
|
}),
|
||||||
|
primary: Some(Style {
|
||||||
|
color: Some(Color::Cyan),
|
||||||
|
}),
|
||||||
|
secondary: Some(Style {
|
||||||
|
color: Some(Color::Yellow),
|
||||||
|
}),
|
||||||
|
success: Some(Style {
|
||||||
|
color: Some(Color::Green),
|
||||||
|
}),
|
||||||
|
system_function: Some(Style {
|
||||||
|
color: Some(Color::Yellow),
|
||||||
|
}),
|
||||||
|
unmonitored: Some(Style {
|
||||||
|
color: Some(Color::Gray),
|
||||||
|
}),
|
||||||
|
unmonitored_missing: Some(Style {
|
||||||
|
color: Some(Color::Yellow),
|
||||||
|
}),
|
||||||
|
unreleased: Some(Style {
|
||||||
|
color: Some(Color::LightCyan),
|
||||||
|
}),
|
||||||
|
warning: Some(Style {
|
||||||
|
color: Some(Color::Magenta),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(Theme::default(), expected_theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_default_theme_definition() {
|
||||||
|
let expected_theme_definition = ThemeDefinition {
|
||||||
|
name: String::new(),
|
||||||
|
theme: Theme::default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(ThemeDefinition::default(), expected_theme_definition);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialization_defaults_to_using_default_theme_values_when_missing() {
|
||||||
|
let theme_yaml = r#""#;
|
||||||
|
let theme: Theme = serde_yaml::from_str(theme_yaml).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(theme, Theme::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_deserialization_does_not_overwrite_non_empty_fields_with_default_values() {
|
||||||
|
let theme_yaml = r###"
|
||||||
|
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: Theme = serde_yaml::from_str(theme_yaml).unwrap();
|
||||||
|
let expected_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, expected_theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_theme_definitions_wrapper_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_theme(),
|
||||||
|
},
|
||||||
|
ThemeDefinition {
|
||||||
|
name: "dracula".to_owned(),
|
||||||
|
theme: dracula_theme(),
|
||||||
|
},
|
||||||
|
ThemeDefinition {
|
||||||
|
name: "eldritch".to_owned(),
|
||||||
|
theme: eldritch_theme(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,25 @@
|
|||||||
use crate::ui::styles::ManagarrStyle;
|
use crate::ui::styles::ManagarrStyle;
|
||||||
|
use crate::ui::THEME;
|
||||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||||
use ratatui::style::{Color, Style, Stylize};
|
use ratatui::style::{Style, Stylize};
|
||||||
use ratatui::symbols;
|
use ratatui::symbols;
|
||||||
use ratatui::text::{Line, Span, Text};
|
use ratatui::text::{Line, Span, Text};
|
||||||
use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap};
|
use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap};
|
||||||
|
|
||||||
pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55);
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "utils_tests.rs"]
|
#[path = "utils_tests.rs"]
|
||||||
mod utils_tests;
|
mod utils_tests;
|
||||||
|
|
||||||
pub fn background_block<'a>() -> Block<'a> {
|
pub fn background_block<'a>() -> Block<'a> {
|
||||||
Block::new().white().bg(COLOR_TEAL)
|
THEME.with(|theme| {
|
||||||
|
let background = theme.get().background.unwrap();
|
||||||
|
|
||||||
|
if background.enabled.unwrap() {
|
||||||
|
Block::new().white().bg(background.color.unwrap())
|
||||||
|
} else {
|
||||||
|
Block::new().white()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_block<'a>() -> Block<'a> {
|
pub fn layout_block<'a>() -> Block<'a> {
|
||||||
@@ -30,11 +37,11 @@ pub fn layout_block_top_border_with_title(title_span: Span<'_>) -> Block<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_block_top_border<'a>() -> Block<'a> {
|
pub fn layout_block_top_border<'a>() -> Block<'a> {
|
||||||
Block::new().borders(Borders::TOP)
|
Block::new().borders(Borders::TOP).default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_block_bottom_border<'a>() -> Block<'a> {
|
pub fn layout_block_bottom_border<'a>() -> Block<'a> {
|
||||||
Block::new().borders(Borders::BOTTOM)
|
Block::new().borders(Borders::BOTTOM).default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
|
pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
|
||||||
@@ -47,7 +54,7 @@ pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn borderless_block<'a>() -> Block<'a> {
|
pub fn borderless_block<'a>() -> Block<'a> {
|
||||||
Block::new()
|
Block::new().default()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn style_block_highlight(is_selected: bool) -> Style {
|
pub fn style_block_highlight(is_selected: bool) -> Style {
|
||||||
@@ -62,16 +69,20 @@ pub fn title_style(title: &str) -> Span<'_> {
|
|||||||
format!(" {title} ").bold()
|
format!(" {title} ").bold()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn title_block(title: &str) -> Block<'_> {
|
pub fn unstyled_title_block(title: &str) -> Block<'_> {
|
||||||
layout_block_with_title(title_style(title))
|
layout_block_with_title(title_style(title))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn title_block(title: &str) -> Block<'_> {
|
||||||
|
unstyled_title_block(title).default()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn title_block_centered(title: &str) -> Block<'_> {
|
pub fn title_block_centered(title: &str) -> Block<'_> {
|
||||||
title_block(title).title_alignment(Alignment::Center)
|
title_block(title).title_alignment(Alignment::Center)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn logo_block<'a>() -> Block<'a> {
|
pub fn logo_block<'a>() -> Block<'a> {
|
||||||
layout_block().title(Span::styled(
|
layout_block().default().title(Span::styled(
|
||||||
" Managarr - A Servarr management TUI ",
|
" Managarr - A Servarr management TUI ",
|
||||||
Style::new().magenta().bold().italic(),
|
Style::new().magenta().bold().italic(),
|
||||||
))
|
))
|
||||||
@@ -98,7 +109,7 @@ pub fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
|
|||||||
pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge<'_> {
|
pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge<'_> {
|
||||||
LineGauge::new()
|
LineGauge::new()
|
||||||
.block(Block::new().title(title))
|
.block(Block::new().title(title))
|
||||||
.filled_style(Style::new().cyan())
|
.filled_style(Style::new().primary())
|
||||||
.line_set(symbols::line::THICK)
|
.line_set(symbols::line::THICK)
|
||||||
.ratio(ratio)
|
.ratio(ratio)
|
||||||
.label(Line::from(format!("{:.0}%", ratio * 100.0)))
|
.label(Line::from(format!("{:.0}%", ratio * 100.0)))
|
||||||
@@ -107,7 +118,7 @@ pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge<'_> {
|
|||||||
pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> {
|
pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> {
|
||||||
LineGauge::new()
|
LineGauge::new()
|
||||||
.block(Block::new())
|
.block(Block::new())
|
||||||
.filled_style(Style::new().cyan())
|
.filled_style(Style::new().primary())
|
||||||
.line_set(symbols::line::THICK)
|
.line_set(symbols::line::THICK)
|
||||||
.ratio(ratio)
|
.ratio(ratio)
|
||||||
.label(Line::from(format!("{title}: {:.0}%", ratio * 100.0)))
|
.label(Line::from(format!("{title}: {:.0}%", ratio * 100.0)))
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use crate::ui::styles::ManagarrStyle;
|
||||||
use crate::ui::utils::{
|
use crate::ui::utils::{
|
||||||
borderless_block, centered_rect, convert_to_minutes_hours_days, decorate_peer_style,
|
borderless_block, centered_rect, convert_to_minutes_hours_days, decorate_peer_style,
|
||||||
get_width_from_percentage, layout_block, layout_block_bottom_border, layout_block_top_border,
|
get_width_from_percentage, layout_block, layout_block_bottom_border, layout_block_top_border,
|
||||||
layout_block_top_border_with_title, layout_block_with_title, logo_block, style_block_highlight,
|
layout_block_top_border_with_title, layout_block_with_title, logo_block, style_block_highlight,
|
||||||
style_log_list_item, title_block, title_block_centered, title_style,
|
style_log_list_item, title_block, title_block_centered, title_style, unstyled_title_block,
|
||||||
};
|
};
|
||||||
use pretty_assertions::{assert_eq, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_str_eq};
|
||||||
use ratatui::layout::{Alignment, Rect};
|
use ratatui::layout::{Alignment, Rect};
|
||||||
@@ -17,7 +18,7 @@ mod test {
|
|||||||
fn test_layout_block() {
|
fn test_layout_block() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
layout_block(),
|
layout_block(),
|
||||||
Block::default()
|
Block::new()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
);
|
);
|
||||||
@@ -27,11 +28,11 @@ mod test {
|
|||||||
fn test_layout_block_with_title() {
|
fn test_layout_block_with_title() {
|
||||||
let title_span = Span::styled(
|
let title_span = Span::styled(
|
||||||
"title",
|
"title",
|
||||||
Style::default()
|
Style::new()
|
||||||
.fg(Color::DarkGray)
|
.fg(Color::DarkGray)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
);
|
);
|
||||||
let expected_block = Block::default()
|
let expected_block = Block::new()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(title_span.clone());
|
.title(title_span.clone());
|
||||||
@@ -43,11 +44,12 @@ mod test {
|
|||||||
fn test_layout_block_top_border_with_title() {
|
fn test_layout_block_top_border_with_title() {
|
||||||
let title_span = Span::styled(
|
let title_span = Span::styled(
|
||||||
"title",
|
"title",
|
||||||
Style::default()
|
Style::new()
|
||||||
.fg(Color::DarkGray)
|
.fg(Color::DarkGray)
|
||||||
.add_modifier(Modifier::BOLD),
|
.add_modifier(Modifier::BOLD),
|
||||||
);
|
);
|
||||||
let expected_block = Block::default()
|
let expected_block = Block::new()
|
||||||
|
.default()
|
||||||
.borders(Borders::TOP)
|
.borders(Borders::TOP)
|
||||||
.title(title_span.clone());
|
.title(title_span.clone());
|
||||||
|
|
||||||
@@ -61,7 +63,7 @@ mod test {
|
|||||||
fn test_layout_block_top_border() {
|
fn test_layout_block_top_border() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
layout_block_top_border(),
|
layout_block_top_border(),
|
||||||
Block::default().borders(Borders::TOP)
|
Block::new().borders(Borders::TOP).default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,48 +71,58 @@ mod test {
|
|||||||
fn test_layout_block_bottom_border() {
|
fn test_layout_block_bottom_border() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
layout_block_bottom_border(),
|
layout_block_bottom_border(),
|
||||||
Block::default().borders(Borders::BOTTOM)
|
Block::new().borders(Borders::BOTTOM).default()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_borderless_block() {
|
fn test_borderless_block() {
|
||||||
assert_eq!(borderless_block(), Block::default());
|
assert_eq!(borderless_block(), Block::new().default());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_style_button_highlight_selected() {
|
fn test_style_button_highlight_selected() {
|
||||||
let expected_style = Style::default()
|
let expected_style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||||
.fg(Color::Yellow)
|
|
||||||
.add_modifier(Modifier::BOLD);
|
|
||||||
|
|
||||||
assert_eq!(style_block_highlight(true), expected_style);
|
assert_eq!(style_block_highlight(true), expected_style);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_style_button_highlight_unselected() {
|
fn test_style_button_highlight_unselected() {
|
||||||
let expected_style = Style::default()
|
let expected_style = Style::new().fg(Color::White).add_modifier(Modifier::BOLD);
|
||||||
.fg(Color::White)
|
|
||||||
.add_modifier(Modifier::BOLD);
|
|
||||||
|
|
||||||
assert_eq!(style_block_highlight(false), expected_style);
|
assert_eq!(style_block_highlight(false), expected_style);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_title_style() {
|
fn test_title_style() {
|
||||||
let expected_span = Span::styled(" test ", Style::default().add_modifier(Modifier::BOLD));
|
let expected_span = Span::styled(" test ", Style::new().add_modifier(Modifier::BOLD));
|
||||||
|
|
||||||
assert_eq!(title_style("test"), expected_span);
|
assert_eq!(title_style("test"), expected_span);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_title_block() {
|
fn test_unstyled_title_block() {
|
||||||
let expected_block = Block::default()
|
let expected_block = Block::new()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(Span::styled(
|
.title(Span::styled(
|
||||||
" test ",
|
" test ",
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
Style::new().add_modifier(Modifier::BOLD),
|
||||||
|
));
|
||||||
|
|
||||||
|
assert_eq!(unstyled_title_block("test"), expected_block);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_title_block() {
|
||||||
|
let expected_block = Block::new()
|
||||||
|
.default()
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_type(BorderType::Rounded)
|
||||||
|
.title(Span::styled(
|
||||||
|
" test ",
|
||||||
|
Style::new().add_modifier(Modifier::BOLD),
|
||||||
));
|
));
|
||||||
|
|
||||||
assert_eq!(title_block("test"), expected_block);
|
assert_eq!(title_block("test"), expected_block);
|
||||||
@@ -118,12 +130,13 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_title_block_centered() {
|
fn test_title_block_centered() {
|
||||||
let expected_block = Block::default()
|
let expected_block = Block::new()
|
||||||
|
.default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(Span::styled(
|
.title(Span::styled(
|
||||||
" test ",
|
" test ",
|
||||||
Style::default().add_modifier(Modifier::BOLD),
|
Style::new().add_modifier(Modifier::BOLD),
|
||||||
))
|
))
|
||||||
.title_alignment(Alignment::Center);
|
.title_alignment(Alignment::Center);
|
||||||
|
|
||||||
@@ -132,12 +145,13 @@ mod test {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_logo_block() {
|
fn test_logo_block() {
|
||||||
let expected_block = Block::default()
|
let expected_block = Block::new()
|
||||||
|
.default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_type(BorderType::Rounded)
|
.border_type(BorderType::Rounded)
|
||||||
.title(Span::styled(
|
.title(Span::styled(
|
||||||
" Managarr - A Servarr management TUI ",
|
" Managarr - A Servarr management TUI ",
|
||||||
Style::default()
|
Style::new()
|
||||||
.fg(Color::Magenta)
|
.fg(Color::Magenta)
|
||||||
.add_modifier(Modifier::BOLD)
|
.add_modifier(Modifier::BOLD)
|
||||||
.add_modifier(Modifier::ITALIC),
|
.add_modifier(Modifier::ITALIC),
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ use super::message::Message;
|
|||||||
use super::popup::Size;
|
use super::popup::Size;
|
||||||
use crate::models::stateful_table::StatefulTable;
|
use crate::models::stateful_table::StatefulTable;
|
||||||
use crate::ui::styles::ManagarrStyle;
|
use crate::ui::styles::ManagarrStyle;
|
||||||
use crate::ui::utils::{centered_rect, layout_block_top_border, title_block_centered};
|
use crate::ui::utils::{
|
||||||
|
borderless_block, centered_rect, layout_block_top_border, title_block_centered,
|
||||||
|
};
|
||||||
use crate::ui::widgets::loading_block::LoadingBlock;
|
use crate::ui::widgets::loading_block::LoadingBlock;
|
||||||
use crate::ui::widgets::popup::Popup;
|
use crate::ui::widgets::popup::Popup;
|
||||||
use crate::ui::widgets::selectable_list::SelectableList;
|
use crate::ui::widgets::selectable_list::SelectableList;
|
||||||
@@ -68,7 +70,7 @@ where
|
|||||||
row_mapper,
|
row_mapper,
|
||||||
footer: None,
|
footer: None,
|
||||||
footer_alignment: Alignment::Left,
|
footer_alignment: Alignment::Left,
|
||||||
block: Block::new(),
|
block: borderless_block(),
|
||||||
margin: 0,
|
margin: 0,
|
||||||
is_loading: false,
|
is_loading: false,
|
||||||
highlight_rows: true,
|
highlight_rows: true,
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ mod tests {
|
|||||||
use crate::models::stateful_list::StatefulList;
|
use crate::models::stateful_list::StatefulList;
|
||||||
use crate::models::stateful_table::{SortOption, StatefulTable};
|
use crate::models::stateful_table::{SortOption, StatefulTable};
|
||||||
use crate::models::{HorizontallyScrollableText, Scrollable};
|
use crate::models::{HorizontallyScrollableText, Scrollable};
|
||||||
|
use crate::ui::utils::borderless_block;
|
||||||
use crate::ui::widgets::managarr_table::ManagarrTable;
|
use crate::ui::widgets::managarr_table::ManagarrTable;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use ratatui::layout::{Alignment, Constraint};
|
use ratatui::layout::{Alignment, Constraint};
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use ratatui::widgets::{Block, Cell, Row};
|
use ratatui::widgets::{Cell, Row};
|
||||||
use std::sync::atomic::AtomicUsize;
|
use std::sync::atomic::AtomicUsize;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -26,7 +27,7 @@ mod tests {
|
|||||||
assert_eq!(managarr_table.constraints, Vec::new());
|
assert_eq!(managarr_table.constraints, Vec::new());
|
||||||
assert_eq!(managarr_table.footer, None);
|
assert_eq!(managarr_table.footer, None);
|
||||||
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
||||||
assert_eq!(managarr_table.block, Block::new());
|
assert_eq!(managarr_table.block, borderless_block());
|
||||||
assert_eq!(managarr_table.margin, 0);
|
assert_eq!(managarr_table.margin, 0);
|
||||||
assert!(!managarr_table.is_loading);
|
assert!(!managarr_table.is_loading);
|
||||||
assert!(managarr_table.highlight_rows);
|
assert!(managarr_table.highlight_rows);
|
||||||
@@ -62,7 +63,7 @@ mod tests {
|
|||||||
assert_eq!(managarr_table.constraints, Vec::new());
|
assert_eq!(managarr_table.constraints, Vec::new());
|
||||||
assert_eq!(managarr_table.footer, None);
|
assert_eq!(managarr_table.footer, None);
|
||||||
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
||||||
assert_eq!(managarr_table.block, Block::new());
|
assert_eq!(managarr_table.block, borderless_block());
|
||||||
assert_eq!(managarr_table.margin, 0);
|
assert_eq!(managarr_table.margin, 0);
|
||||||
assert!(!managarr_table.is_loading);
|
assert!(!managarr_table.is_loading);
|
||||||
assert!(managarr_table.highlight_rows);
|
assert!(managarr_table.highlight_rows);
|
||||||
@@ -98,7 +99,7 @@ mod tests {
|
|||||||
assert_eq!(managarr_table.constraints, Vec::new());
|
assert_eq!(managarr_table.constraints, Vec::new());
|
||||||
assert_eq!(managarr_table.footer, None);
|
assert_eq!(managarr_table.footer, None);
|
||||||
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
||||||
assert_eq!(managarr_table.block, Block::new());
|
assert_eq!(managarr_table.block, borderless_block());
|
||||||
assert_eq!(managarr_table.margin, 0);
|
assert_eq!(managarr_table.margin, 0);
|
||||||
assert!(!managarr_table.is_loading);
|
assert!(!managarr_table.is_loading);
|
||||||
assert!(managarr_table.highlight_rows);
|
assert!(managarr_table.highlight_rows);
|
||||||
@@ -131,7 +132,7 @@ mod tests {
|
|||||||
assert_eq!(managarr_table.constraints, Vec::new());
|
assert_eq!(managarr_table.constraints, Vec::new());
|
||||||
assert_eq!(managarr_table.footer, None);
|
assert_eq!(managarr_table.footer, None);
|
||||||
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
||||||
assert_eq!(managarr_table.block, Block::new());
|
assert_eq!(managarr_table.block, borderless_block());
|
||||||
assert_eq!(managarr_table.margin, 0);
|
assert_eq!(managarr_table.margin, 0);
|
||||||
assert!(!managarr_table.is_loading);
|
assert!(!managarr_table.is_loading);
|
||||||
assert!(managarr_table.highlight_rows);
|
assert!(managarr_table.highlight_rows);
|
||||||
@@ -164,7 +165,7 @@ mod tests {
|
|||||||
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
|
assert_eq!(managarr_table.table_headers, Vec::<String>::new());
|
||||||
assert_eq!(managarr_table.footer, None);
|
assert_eq!(managarr_table.footer, None);
|
||||||
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
assert_eq!(managarr_table.footer_alignment, Alignment::Left);
|
||||||
assert_eq!(managarr_table.block, Block::new());
|
assert_eq!(managarr_table.block, borderless_block());
|
||||||
assert_eq!(managarr_table.margin, 0);
|
assert_eq!(managarr_table.margin, 0);
|
||||||
assert!(!managarr_table.is_loading);
|
assert!(!managarr_table.is_loading);
|
||||||
assert!(managarr_table.highlight_rows);
|
assert!(managarr_table.highlight_rows);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use crate::app::{log_and_print_error, App, AppConfig};
|
use crate::app::{log_and_print_error, App, AppConfig};
|
||||||
use crate::cli::{self, Command};
|
use crate::cli::{self, Command};
|
||||||
use crate::network::Network;
|
use crate::network::Network;
|
||||||
|
use crate::ui::theme::ThemeDefinitionsWrapper;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "utils_tests.rs"]
|
#[path = "utils_tests.rs"]
|
||||||
@@ -140,11 +141,32 @@ fn colorize_log_line(line: &str, re: &Regex) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn load_config(path: &str) -> Result<AppConfig> {
|
pub(super) fn load_config(path: &str) -> Result<AppConfig> {
|
||||||
let file = File::open(path).map_err(|e| anyhow!(e))?;
|
match File::open(path).map_err(|e| anyhow!(e)) {
|
||||||
|
Ok(file) => {
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
let config = serde_yaml::from_reader(reader)?;
|
let config = serde_yaml::from_reader(reader)?;
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_and_print_error(format!("Unable to open config file: {e:?}"));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let theme_config = serde_yaml::from_reader(reader)?;
|
||||||
|
Ok(theme_config)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log_and_print_error(format!("Unable to open theme file: {e:?}"));
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn build_network_client(config: &AppConfig) -> Client {
|
pub(super) fn build_network_client(config: &AppConfig) -> Client {
|
||||||
let mut client_builder = Client::builder()
|
let mut client_builder = Client::builder()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ fn test_derive_enum_display_style() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(EnumDisplayStyle)]
|
#[derive(EnumDisplayStyle)]
|
||||||
pub enum TestEnum {
|
enum TestEnum {
|
||||||
#[display_style(name = "Testing 123")]
|
#[display_style(name = "Testing 123")]
|
||||||
Test,
|
Test,
|
||||||
Ignored,
|
Ignored,
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use validate_theme_derive::ValidateTheme;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_theme_derive() {
|
||||||
|
let theme = Theme {
|
||||||
|
name: "test".to_string(),
|
||||||
|
good: Some(Style {
|
||||||
|
color: "Green".to_owned(),
|
||||||
|
}),
|
||||||
|
bad: Some(Style {
|
||||||
|
color: "Red".to_owned(),
|
||||||
|
}),
|
||||||
|
ugly: Some(Style {
|
||||||
|
color: "Magenta".to_owned(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
theme.validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
struct Style {
|
||||||
|
color: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(ValidateTheme)]
|
||||||
|
struct Theme {
|
||||||
|
pub name: String,
|
||||||
|
#[validate]
|
||||||
|
pub good: Option<Style>,
|
||||||
|
#[validate]
|
||||||
|
pub bad: Option<Style>,
|
||||||
|
pub ugly: Option<Style>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# 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)
|
||||||
|

|
||||||
|
|
||||||
|
### [Eldritch](./eldritch/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:
|
||||||
|
# Disable for transparent backgrounds
|
||||||
|
enabled: true
|
||||||
|
# Color of the full system background
|
||||||
|
color: "#233237"
|
||||||
|
awaiting_import:
|
||||||
|
# Color for items awaiting import
|
||||||
|
color: "#FFAA42"
|
||||||
|
indeterminate:
|
||||||
|
# Color for when item status is unknown
|
||||||
|
color: "#FFAA42"
|
||||||
|
default:
|
||||||
|
# Default color for text and uncolored elements
|
||||||
|
color: "#FFFFFF"
|
||||||
|
downloaded:
|
||||||
|
# Color for downloaded items (when monitored)
|
||||||
|
color: "#00FF00"
|
||||||
|
downloading:
|
||||||
|
# Color for items currently downloading
|
||||||
|
color: "#762671"
|
||||||
|
failure:
|
||||||
|
# Color for errors, no seeders, disabled indexers,
|
||||||
|
# failed indexer test results, etc.
|
||||||
|
color: "#DE382B"
|
||||||
|
help:
|
||||||
|
# Color for help text like hotkeys and tooltips
|
||||||
|
color: "#00FFFF"
|
||||||
|
missing:
|
||||||
|
# Color for missing items
|
||||||
|
color: "#DE382B"
|
||||||
|
primary:
|
||||||
|
# Primary color for table/list items (without custom coloring),
|
||||||
|
# selected UI elements (e.g. table/list items, current tab, etc.),
|
||||||
|
# gauge bars, etc.
|
||||||
|
color: "#2CB5E9"
|
||||||
|
secondary:
|
||||||
|
# Color for selected tabs, Warn log events, Paragraph headers (e.g. "Name: "),
|
||||||
|
# and uncolored Message box contents
|
||||||
|
color: "#FFC706"
|
||||||
|
success:
|
||||||
|
# Color for downloaded and imported and available items,
|
||||||
|
# good peer styling (more seeders than leechers),
|
||||||
|
# passing indexer test results, and enabled indexers
|
||||||
|
color: "#39B54A"
|
||||||
|
system_function:
|
||||||
|
# Color for system functions like selected UI elements (e.g. checkboxes, yes/no prompts),
|
||||||
|
# or loading spinner
|
||||||
|
color: "#FFC706"
|
||||||
|
unmonitored:
|
||||||
|
# Color for unmonitored items
|
||||||
|
color: "#808080"
|
||||||
|
unmonitored_missing:
|
||||||
|
# Color for unmonitored items that are also missing
|
||||||
|
color: "#FFC706"
|
||||||
|
unreleased:
|
||||||
|
# Color for unreleased items
|
||||||
|
color: "#00FFFF"
|
||||||
|
warning:
|
||||||
|
# Color for fewer seeders than leechers
|
||||||
|
color: "#FF00FF"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note:** Bear in mind that while some colors may currently affect few UI elements, Managarr is still being built
|
||||||
|
and thus these colors may affect more elements in the future.
|
||||||
|
|
||||||
|
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,8 @@
|
|||||||
|
# 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: 220 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,8 @@
|
|||||||
|
# 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: 345 KiB |
|
After Width: | Height: | Size: 354 KiB |
|
After Width: | Height: | Size: 223 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
- name: dracula
|
||||||
|
theme:
|
||||||
|
background:
|
||||||
|
enabled: true
|
||||||
|
color: "#232326"
|
||||||
|
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,8 @@
|
|||||||
|
# Managarr Eldritch Theme
|
||||||
|
The [themes.yml](./themes.yml) file in this directory corresponds to the theme configuration for the Eldritch Managarr theme.
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 374 KiB |
|
After Width: | Height: | Size: 224 KiB |
@@ -0,0 +1,29 @@
|
|||||||
|
- name: eldritch
|
||||||
|
theme:
|
||||||
|
background:
|
||||||
|
enabled: true
|
||||||
|
color: "#212337"
|
||||||
|
default:
|
||||||
|
color: "#ebfafa"
|
||||||
|
downloaded:
|
||||||
|
color: "#37f499"
|
||||||
|
downloading:
|
||||||
|
color: "#f7c67f"
|
||||||
|
failure:
|
||||||
|
color: "#f16c75"
|
||||||
|
missing:
|
||||||
|
color: "#f7c67f"
|
||||||
|
unmonitored_missing:
|
||||||
|
color: "#7081d0"
|
||||||
|
help:
|
||||||
|
color: "#7081d0"
|
||||||
|
primary:
|
||||||
|
color: "#f265b5"
|
||||||
|
secondary:
|
||||||
|
color: "#04d1f9"
|
||||||
|
success:
|
||||||
|
color: "#37f499"
|
||||||
|
warning:
|
||||||
|
color: "#f1fc79"
|
||||||
|
unreleased:
|
||||||
|
color: "#ebfafa"
|
||||||
@@ -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: 389 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"
|
||||||