feat: Initial support for custom user-defined themes
This commit is contained in:
@@ -31,6 +31,7 @@ mod tests {
|
||||
};
|
||||
let sonarr_config_2 = ServarrConfig::default();
|
||||
let config = AppConfig {
|
||||
theme: None,
|
||||
radarr: Some(vec![radarr_config_1.clone(), radarr_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)]
|
||||
pub struct AppConfig {
|
||||
pub theme: Option<String>,
|
||||
pub radarr: Option<Vec<ServarrConfig>>,
|
||||
pub sonarr: Option<Vec<ServarrConfig>>,
|
||||
}
|
||||
|
||||
+59
-10
@@ -1,10 +1,4 @@
|
||||
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_complete::generate;
|
||||
use crossterm::execute;
|
||||
@@ -16,6 +10,11 @@ use network::NetworkTrait;
|
||||
use ratatui::backend::CrosstermBackend;
|
||||
use ratatui::Terminal;
|
||||
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::sync::mpsc::Receiver;
|
||||
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,
|
||||
};
|
||||
|
||||
use crate::app::App;
|
||||
use crate::app::{log_and_print_error, App};
|
||||
use crate::cli::Command;
|
||||
use crate::event::input_event::{Events, InputEvent};
|
||||
use crate::event::Key;
|
||||
use crate::network::{Network, NetworkEvent};
|
||||
use crate::ui::ui;
|
||||
use crate::ui::theme::{Theme, ThemeDefinition};
|
||||
use crate::ui::{ui, THEME};
|
||||
use crate::utils::load_theme_config;
|
||||
|
||||
mod app;
|
||||
mod cli;
|
||||
@@ -74,6 +75,22 @@ struct Cli {
|
||||
help = "The Managarr configuration file to use"
|
||||
)]
|
||||
config_file: Option<PathBuf>,
|
||||
#[arg(
|
||||
long,
|
||||
global = true,
|
||||
value_parser,
|
||||
env = "MANAGARR_THEME_FILE",
|
||||
help = "The Managarr theme file to use"
|
||||
)]
|
||||
theme_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(
|
||||
long,
|
||||
global = true,
|
||||
@@ -98,10 +115,12 @@ async fn main() -> Result<()> {
|
||||
} else {
|
||||
confy::load("managarr", "config")?
|
||||
};
|
||||
let theme_name = config.theme.clone();
|
||||
let spinner_disabled = args.disable_spinner;
|
||||
debug!("Managarr loaded using config: {config:?}");
|
||||
config.validate();
|
||||
config.post_process_initialization();
|
||||
|
||||
let reqwest_client = build_network_client(&config);
|
||||
let (sync_network_tx, sync_network_rx) = mpsc::channel(500);
|
||||
let cancellation_token = CancellationToken::new();
|
||||
@@ -140,7 +159,12 @@ async fn main() -> Result<()> {
|
||||
std::thread::spawn(move || {
|
||||
start_networking(sync_network_rx, &app_nw, cancellation_token, reqwest_client)
|
||||
});
|
||||
start_ui(&app).await?;
|
||||
start_ui(
|
||||
&app,
|
||||
&args.theme_file,
|
||||
args.theme.unwrap_or(theme_name.unwrap_or_default()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +198,32 @@ async fn start_networking(
|
||||
}
|
||||
}
|
||||
|
||||
async fn start_ui(app: &Arc<Mutex<App<'_>>>) -> Result<()> {
|
||||
async fn start_ui(
|
||||
app: &Arc<Mutex<App<'_>>>,
|
||||
theme_file_arg: &Option<PathBuf>,
|
||||
theme_name: String,
|
||||
) -> Result<()> {
|
||||
let theme_definitions = if let Some(ref theme_file) = theme_file_arg {
|
||||
load_theme_config(theme_file.to_str().expect("Invalid theme file specified"))?
|
||||
} else {
|
||||
confy::load("managarr", "theme").unwrap_or_else(|_| vec![ThemeDefinition::default()])
|
||||
};
|
||||
let theme = if !theme_name.is_empty() {
|
||||
let theme_definition = 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.set(theme);
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
enable_raw_mode()?;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cell::Cell;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use ratatui::layout::{Constraint, Flex, Layout, Rect};
|
||||
@@ -15,6 +16,7 @@ use crate::app::App;
|
||||
use crate::models::{HorizontallyScrollableText, Route, TabState};
|
||||
use crate::ui::radarr_ui::RadarrUi;
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::theme::Theme;
|
||||
use crate::ui::utils::{
|
||||
background_block, borderless_block, centered_rect, logo_block, title_block, title_block_centered,
|
||||
};
|
||||
@@ -24,10 +26,14 @@ use crate::ui::widgets::popup::Size;
|
||||
mod radarr_ui;
|
||||
mod sonarr_ui;
|
||||
mod styles;
|
||||
pub mod theme;
|
||||
mod utils;
|
||||
mod widgets;
|
||||
|
||||
static HIGHLIGHT_SYMBOL: &str = "=> ";
|
||||
thread_local! {
|
||||
pub static THEME: Cell<Theme> = Cell::new(Theme::default());
|
||||
}
|
||||
|
||||
pub trait DrawUi {
|
||||
fn accepts(route: Route) -> bool;
|
||||
|
||||
+17
-19
@@ -1,8 +1,6 @@
|
||||
use ratatui::prelude::Color;
|
||||
use crate::ui::THEME;
|
||||
use ratatui::style::{Styled, Stylize};
|
||||
|
||||
pub const COLOR_ORANGE: Color = Color::Rgb(255, 170, 66);
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "styles_tests.rs"]
|
||||
mod styles_tests;
|
||||
@@ -42,31 +40,31 @@ where
|
||||
}
|
||||
|
||||
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 {
|
||||
self.fg(COLOR_ORANGE)
|
||||
THEME.with(|theme| self.fg(theme.get().indeterminate.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn default(self) -> T {
|
||||
self.white()
|
||||
THEME.with(|theme| self.fg(theme.get().default.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn downloaded(self) -> T {
|
||||
self.green()
|
||||
THEME.with(|theme| self.fg(theme.get().downloaded.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn downloading(self) -> T {
|
||||
self.magenta()
|
||||
THEME.with(|theme| self.fg(theme.get().downloading.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn failure(self) -> T {
|
||||
self.red()
|
||||
THEME.with(|theme| self.fg(theme.get().failure.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn help(self) -> T {
|
||||
self.light_blue()
|
||||
THEME.with(|theme| self.fg(theme.get().help.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn highlight(self) -> T {
|
||||
@@ -74,38 +72,38 @@ where
|
||||
}
|
||||
|
||||
fn missing(self) -> T {
|
||||
self.red()
|
||||
THEME.with(|theme| self.fg(theme.get().missing.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn primary(self) -> T {
|
||||
self.cyan()
|
||||
THEME.with(|theme| self.fg(theme.get().primary.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn secondary(self) -> T {
|
||||
self.yellow()
|
||||
THEME.with(|theme| self.fg(theme.get().secondary.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn success(self) -> T {
|
||||
self.green()
|
||||
THEME.with(|theme| self.fg(theme.get().success.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn system_function(self) -> T {
|
||||
self.yellow()
|
||||
THEME.with(|theme| self.fg(theme.get().system_function.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn unmonitored(self) -> T {
|
||||
self.gray()
|
||||
THEME.with(|theme| self.fg(theme.get().unmonitored.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn unmonitored_missing(self) -> T {
|
||||
self.yellow()
|
||||
THEME.with(|theme| self.fg(theme.get().unmonitored_missing.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn unreleased(self) -> T {
|
||||
self.light_cyan()
|
||||
THEME.with(|theme| self.fg(theme.get().unreleased.unwrap().color.unwrap()))
|
||||
}
|
||||
|
||||
fn warning(self) -> T {
|
||||
self.magenta()
|
||||
THEME.with(|theme| self.fg(theme.get().warning.unwrap().color.unwrap()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::ui::styles::{ManagarrStyle, COLOR_ORANGE};
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use pretty_assertions::assert_eq;
|
||||
use ratatui::prelude::Modifier;
|
||||
use ratatui::style::{Style, Stylize};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
|
||||
#[test]
|
||||
fn test_new() {
|
||||
@@ -14,13 +14,16 @@ mod test {
|
||||
fn test_style_awaiting_import() {
|
||||
assert_eq!(
|
||||
Style::new().awaiting_import(),
|
||||
Style::new().fg(COLOR_ORANGE)
|
||||
Style::new().fg(Color::Rgb(255, 170, 66))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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]
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
use anyhow::Result;
|
||||
use derivative::Derivative;
|
||||
use ratatui::style::Color;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "theme_tests.rs"]
|
||||
mod theme_tests;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Derivative)]
|
||||
#[derivative(Default)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct Background {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color_str",
|
||||
serialize_with = "serialize_color_str",
|
||||
default = "default_background_color"
|
||||
)]
|
||||
#[derivative(Default(value = "Some(Color::Rgb(35, 50, 55))"))]
|
||||
pub color: Option<Color>,
|
||||
#[derivative(Default(value = "Some(true)"))]
|
||||
#[serde(default = "default_background_enabled")]
|
||||
pub enabled: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Default, Clone, Copy)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct Style {
|
||||
#[serde(
|
||||
deserialize_with = "deserialize_color_str",
|
||||
serialize_with = "serialize_color_str"
|
||||
)]
|
||||
pub color: Option<Color>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Derivative)]
|
||||
#[derivative(Default)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct Theme {
|
||||
#[serde(default = "default_background")]
|
||||
#[derivative(Default(
|
||||
value = "Some(Background { color: Some(Color::Rgb(35, 50, 55)), enabled: Some(true) })"
|
||||
))]
|
||||
pub background: Option<Background>,
|
||||
#[serde(default = "default_awaiting_import_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Rgb(255, 170, 66)) })"))]
|
||||
pub awaiting_import: Option<Style>,
|
||||
#[serde(default = "default_indeterminate_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Rgb(255, 170, 66)) })"))]
|
||||
pub indeterminate: Option<Style>,
|
||||
#[serde(default = "default_default_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::White) })"))]
|
||||
pub default: Option<Style>,
|
||||
#[serde(default = "default_downloaded_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Green) })"))]
|
||||
pub downloaded: Option<Style>,
|
||||
#[serde(default = "default_downloading_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Magenta) })"))]
|
||||
pub downloading: Option<Style>,
|
||||
#[serde(default = "default_failure_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Red) })"))]
|
||||
pub failure: Option<Style>,
|
||||
#[serde(default = "default_help_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::LightBlue) })"))]
|
||||
pub help: Option<Style>,
|
||||
#[serde(default = "default_missing_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Red) })"))]
|
||||
pub missing: Option<Style>,
|
||||
#[serde(default = "default_primary_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Cyan) })"))]
|
||||
pub primary: Option<Style>,
|
||||
#[serde(default = "default_secondary_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
|
||||
pub secondary: Option<Style>,
|
||||
#[serde(default = "default_success_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Green) })"))]
|
||||
pub success: Option<Style>,
|
||||
#[serde(default = "default_system_function_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
|
||||
pub system_function: Option<Style>,
|
||||
#[serde(default = "default_unmonitored_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Gray) })"))]
|
||||
pub unmonitored: Option<Style>,
|
||||
#[serde(default = "default_unmonitored_missing_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Yellow) })"))]
|
||||
pub unmonitored_missing: Option<Style>,
|
||||
#[serde(default = "default_unreleased_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::LightCyan) })"))]
|
||||
pub unreleased: Option<Style>,
|
||||
#[serde(default = "default_warning_style")]
|
||||
#[derivative(Default(value = "Some(Style { color: Some(Color::Magenta) })"))]
|
||||
pub warning: Option<Style>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, Clone, Default)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
pub struct ThemeDefinition {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub theme: Theme,
|
||||
}
|
||||
|
||||
fn default_background_color() -> Option<Color> {
|
||||
Some(Color::Rgb(35, 50, 55))
|
||||
}
|
||||
|
||||
fn default_background_enabled() -> Option<bool> {
|
||||
Some(true)
|
||||
}
|
||||
|
||||
fn default_background() -> Option<Background> {
|
||||
Some(Background {
|
||||
color: default_background_color(),
|
||||
enabled: Some(true),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_awaiting_import_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Rgb(255, 170, 66)),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_indeterminate_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Rgb(255, 170, 66)),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_default_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::White),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_downloaded_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Green),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_downloading_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Magenta),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_failure_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Red),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_help_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::LightBlue),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_missing_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Red),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_primary_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Cyan),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_secondary_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Yellow),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_success_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Green),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_system_function_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Yellow),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_unmonitored_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Gray),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_unmonitored_missing_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Yellow),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_unreleased_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::LightCyan),
|
||||
})
|
||||
}
|
||||
|
||||
fn default_warning_style() -> Option<Style> {
|
||||
Some(Style {
|
||||
color: Some(Color::Magenta),
|
||||
})
|
||||
}
|
||||
|
||||
fn deserialize_color_str<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s: Option<String> = Option::deserialize(deserializer)?;
|
||||
match s {
|
||||
Some(s) => Color::from_str(&s)
|
||||
.map_err(serde::de::Error::custom)
|
||||
.map(Some),
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_color_str<S>(color: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(&color.unwrap().to_string())
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
mod tests {
|
||||
use crate::ui::theme::{Background, Style, Theme, ThemeDefinition};
|
||||
use pretty_assertions::assert_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);
|
||||
}
|
||||
}
|
||||
+17
-9
@@ -1,22 +1,30 @@
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::THEME;
|
||||
use ratatui::layout::{Alignment, Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style, Stylize};
|
||||
use ratatui::style::{Style, Stylize};
|
||||
use ratatui::symbols;
|
||||
use ratatui::text::{Line, Span, Text};
|
||||
use ratatui::widgets::{Block, BorderType, Borders, LineGauge, ListItem, Paragraph, Wrap};
|
||||
|
||||
pub const COLOR_TEAL: Color = Color::Rgb(35, 50, 55);
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "utils_tests.rs"]
|
||||
mod utils_tests;
|
||||
|
||||
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> {
|
||||
Block::new()
|
||||
.default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
}
|
||||
@@ -30,11 +38,11 @@ pub fn layout_block_top_border_with_title(title_span: Span<'_>) -> Block<'_> {
|
||||
}
|
||||
|
||||
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> {
|
||||
Block::new().borders(Borders::BOTTOM)
|
||||
Block::new().borders(Borders::BOTTOM).default()
|
||||
}
|
||||
|
||||
pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
|
||||
@@ -47,7 +55,7 @@ pub fn layout_paragraph_borderless(string: &str) -> Paragraph<'_> {
|
||||
}
|
||||
|
||||
pub fn borderless_block<'a>() -> Block<'a> {
|
||||
Block::new()
|
||||
Block::new().default()
|
||||
}
|
||||
|
||||
pub fn style_block_highlight(is_selected: bool) -> Style {
|
||||
@@ -98,7 +106,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<'_> {
|
||||
LineGauge::new()
|
||||
.block(Block::new().title(title))
|
||||
.filled_style(Style::new().cyan())
|
||||
.filled_style(Style::new().primary())
|
||||
.line_set(symbols::line::THICK)
|
||||
.ratio(ratio)
|
||||
.label(Line::from(format!("{:.0}%", ratio * 100.0)))
|
||||
@@ -107,7 +115,7 @@ pub fn line_gauge_with_title(title: &str, ratio: f64) -> LineGauge<'_> {
|
||||
pub fn line_gauge_with_label(title: &str, ratio: f64) -> LineGauge<'_> {
|
||||
LineGauge::new()
|
||||
.block(Block::new())
|
||||
.filled_style(Style::new().cyan())
|
||||
.filled_style(Style::new().primary())
|
||||
.line_set(symbols::line::THICK)
|
||||
.ratio(ratio)
|
||||
.label(Line::from(format!("{title}: {:.0}%", ratio * 100.0)))
|
||||
|
||||
+24
-21
@@ -1,5 +1,6 @@
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::ui::styles::ManagarrStyle;
|
||||
use crate::ui::utils::{
|
||||
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,
|
||||
@@ -17,7 +18,8 @@ mod test {
|
||||
fn test_layout_block() {
|
||||
assert_eq!(
|
||||
layout_block(),
|
||||
Block::default()
|
||||
Block::new()
|
||||
.default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
);
|
||||
@@ -27,11 +29,12 @@ mod test {
|
||||
fn test_layout_block_with_title() {
|
||||
let title_span = Span::styled(
|
||||
"title",
|
||||
Style::default()
|
||||
Style::new()
|
||||
.fg(Color::DarkGray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let expected_block = Block::default()
|
||||
let expected_block = Block::new()
|
||||
.default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(title_span.clone());
|
||||
@@ -43,11 +46,12 @@ mod test {
|
||||
fn test_layout_block_top_border_with_title() {
|
||||
let title_span = Span::styled(
|
||||
"title",
|
||||
Style::default()
|
||||
Style::new()
|
||||
.fg(Color::DarkGray)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
);
|
||||
let expected_block = Block::default()
|
||||
let expected_block = Block::new()
|
||||
.default()
|
||||
.borders(Borders::TOP)
|
||||
.title(title_span.clone());
|
||||
|
||||
@@ -61,7 +65,7 @@ mod test {
|
||||
fn test_layout_block_top_border() {
|
||||
assert_eq!(
|
||||
layout_block_top_border(),
|
||||
Block::default().borders(Borders::TOP)
|
||||
Block::new().borders(Borders::TOP).default()
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,48 +73,45 @@ mod test {
|
||||
fn test_layout_block_bottom_border() {
|
||||
assert_eq!(
|
||||
layout_block_bottom_border(),
|
||||
Block::default().borders(Borders::BOTTOM)
|
||||
Block::new().borders(Borders::BOTTOM).default()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_borderless_block() {
|
||||
assert_eq!(borderless_block(), Block::default());
|
||||
assert_eq!(borderless_block(), Block::new().default());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_button_highlight_selected() {
|
||||
let expected_style = Style::default()
|
||||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let expected_style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
|
||||
assert_eq!(style_block_highlight(true), expected_style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_style_button_highlight_unselected() {
|
||||
let expected_style = Style::default()
|
||||
.fg(Color::White)
|
||||
.add_modifier(Modifier::BOLD);
|
||||
let expected_style = Style::new().fg(Color::White).add_modifier(Modifier::BOLD);
|
||||
|
||||
assert_eq!(style_block_highlight(false), expected_style);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_title_block() {
|
||||
let expected_block = Block::default()
|
||||
let expected_block = Block::new()
|
||||
.default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(Span::styled(
|
||||
" test ",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
Style::new().add_modifier(Modifier::BOLD),
|
||||
));
|
||||
|
||||
assert_eq!(title_block("test"), expected_block);
|
||||
@@ -118,12 +119,13 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_title_block_centered() {
|
||||
let expected_block = Block::default()
|
||||
let expected_block = Block::new()
|
||||
.default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(Span::styled(
|
||||
" test ",
|
||||
Style::default().add_modifier(Modifier::BOLD),
|
||||
Style::new().add_modifier(Modifier::BOLD),
|
||||
))
|
||||
.title_alignment(Alignment::Center);
|
||||
|
||||
@@ -132,12 +134,13 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_logo_block() {
|
||||
let expected_block = Block::default()
|
||||
let expected_block = Block::new()
|
||||
.default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Rounded)
|
||||
.title(Span::styled(
|
||||
" Managarr - A Servarr management TUI ",
|
||||
Style::default()
|
||||
Style::new()
|
||||
.fg(Color::Magenta)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
.add_modifier(Modifier::ITALIC),
|
||||
|
||||
+26
-4
@@ -21,6 +21,7 @@ use tokio_util::sync::CancellationToken;
|
||||
use crate::app::{log_and_print_error, App, AppConfig};
|
||||
use crate::cli::{self, Command};
|
||||
use crate::network::Network;
|
||||
use crate::ui::theme::ThemeDefinition;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "utils_tests.rs"]
|
||||
@@ -140,10 +141,31 @@ fn colorize_log_line(line: &str, re: &Regex) -> String {
|
||||
}
|
||||
|
||||
pub(super) fn load_config(path: &str) -> Result<AppConfig> {
|
||||
let file = File::open(path).map_err(|e| anyhow!(e))?;
|
||||
let reader = BufReader::new(file);
|
||||
let config = serde_yaml::from_reader(reader)?;
|
||||
Ok(config)
|
||||
match File::open(path).map_err(|e| anyhow!(e)) {
|
||||
Ok(file) => {
|
||||
let reader = BufReader::new(file);
|
||||
let config = serde_yaml::from_reader(reader)?;
|
||||
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<Vec<ThemeDefinition>> {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user