use std::collections::HashMap;
use std::path::PathBuf;
use std::fs::read_to_string;
use std::io::Write;
use crate::args::WallustArgs;
use crate::args::Globals;
use crate::colors::Colors;
use crate::template;
use crate::template::TemplateFields;
use anyhow::{Result, Context};
use owo_colors::{AnsiColors, OwoColorize};
use serde::Deserialize;
#[derive(Debug, Deserialize, Default)]
#[cfg_attr(feature = "doc" , derive(documented::Documented, documented::DocumentedFields))]
pub struct Config {
#[serde(default)]
#[serde(deserialize_with = "validate_threshold")]
pub threshold: Option<u8>,
#[serde(rename = "backend")]
pub backend_user: Option<crate::backends::Backend>,
#[serde(rename = "palette")]
pub palette_user: Option<crate::palettes::Palette>,
#[serde(rename = "color_space")]
pub color_space_user: Option<crate::colorspaces::ColorSpace>,
pub alpha: Option<u8>,
pub check_contrast: Option<bool>,
pub saturation: Option<u8>,
pub fallback_generator: Option<crate::colorspaces::FallbackGenerator>,
pub templates: Option<HashMap<String, Fields>>,
pub env_vars: Option<bool>,
pub no_hooks: bool,
pub hooks: Option<HashMap<String, String>>,
#[deprecated]
pub entry: Option<Vec<Entries>>,
#[serde(skip)]
pub dir: PathBuf,
#[serde(skip)]
pub file: PathBuf,
#[serde(skip)]
pub templates_dir: PathBuf,
#[serde(skip)]
pub true_th: u8,
#[serde(skip)]
pub backend: crate::backends::Backend,
#[serde(skip)]
pub color_space: crate::colorspaces::ColorSpace,
#[serde(skip)]
pub palette: crate::palettes::Palette,
}
#[derive(Debug, Deserialize, Default)]
#[cfg_attr(feature = "schema" , derive(schemars::JsonSchema))]
pub struct PrettyConfig {
#[serde(default)]
#[serde(deserialize_with = "validate_threshold")]
pub threshold: Option<u8>,
pub backend: Option<crate::backends::Backend>,
pub palette: Option<crate::palettes::Palette>,
pub color_space: Option<crate::colorspaces::ColorSpace>,
pub alpha: Option<u8>,
pub check_contrast: Option<bool>,
pub saturation: Option<u8>,
pub fallback_generator: Option<crate::colorspaces::FallbackGenerator>,
pub templates: Option<HashMap<String, Fields>>,
pub env_vars: Option<bool>,
pub no_hooks: Option<bool>,
pub hooks: Option<HashMap<String, String>>,
}
#[derive(Debug, Deserialize, Clone)]
#[cfg_attr(feature = "schema" , derive(schemars::JsonSchema))]
pub struct Fields {
#[serde(alias = "src")]
pub template: String,
#[serde(alias = "dst")]
pub target: String,
pub pywal: Option<bool>,
pub max_depth: Option<u8>,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Entries {
pub template: String,
pub target: String,
pub new_engine: Option<bool>,
}
pub enum WalStr {
Path(PathBuf),
Theme(String),
}
pub const V3: &str = "<https://explosion-mental.codeberg.page/wallust/v3.html>";
impl Config {
pub fn new(g: &Globals) -> Result<Config> {
let dir = match &g.config_dir {
Some(s) => s,
None => {
let Some(original_config_path) = dirs::config_dir() else {
anyhow::bail!("Config path for the platform could not be found.");
};
&original_config_path.join("wallust")
}
};
let config = match &g.config_file {
Some(s) => {
if !s.exists() { anyhow::bail!("Configuration file provided doesn't exist: {}", s.display()); }
s
},
None => &dir.join("wallust.toml"),
};
let templates_dir = match &g.templates_dir {
Some(s) => {
if !s.exists() { anyhow::bail!("Templates dir provided doesn't exist: {}", s.display()); }
s
},
None => &dir.join("templates"),
};
let mut ret = if g.no_config { println!("[{info}] {t}: Not using a configuration file, using default values.", info = "I".blue().bold(), t = "config".magenta().bold());
Config::default()
} else {
if !config.exists() { std::fs::create_dir_all(dir).with_context(|| format!("Failed to create {}", config.display()))?;
std::fs::File::create(config)?
.write_all(include_bytes!("../wallust.toml"))?;
println!("[{info}] {t}: Configuration file {nf}, creating one at {c}",
info = "I".blue().bold(), t = "config".magenta().bold(), nf = "not found".bold().blue(), c = config.display().italic());
}
let s = || format!("Failed to read file {}:\nIf you are switching from v2 to v3, use `wallust migrate`.\nMake sure to read {V3} as well.", config.display());
let toml: PrettyConfig = toml::from_str(
&read_to_string(config)
.with_context(s)?
).with_context(s)?;
toml.into()
};
ret.templates_dir = templates_dir.into();
ret.dir = dir.into();
ret.file = config.into();
ret.true_th = 0;
ret.backend = ret.backend_user.unwrap_or_default();
ret.color_space = ret.color_space_user.unwrap_or_default();
ret.palette = ret.palette_user.unwrap_or_default();
ret.no_hooks = if ret.no_hooks == false { g.no_hooks } else { ret.no_hooks };
Ok(ret)
}
pub fn print(&self) {
let k = if self.check_contrast.unwrap_or(false) {
format!("\n[{}] {}: Doing extra calculations to ensure a good contrast",
"I".blue().bold(),
"contrast".magenta().bold()
)
} else { String::new() };
let sat = if let Some(s) = self.saturation {
format!("\n[{}] {}: Adding saturation to existing palette by {s}%",
"I".blue().bold(),
"saturation".magenta().bold()
)
} else { String::new() };
let th = match self.threshold {
Some(s) => format!("Using a threshold of {s} in between colors."),
None => format!("Not defined, using {} default thresholds.", "best".bold()),
};
println!(
"[{i}] {back_f}: Using {back} backend parser
[{i}] {th_f}: {th}
[{i}] {cs_f}: Using {cs} colorspace variation
[{i}] {palette_f}: Using {palette} palette{k}{sat}",
back = self.backend.bold().color(self.backend.col()),
palette = self.palette.bold().color(self.palette.col()),
cs = self.color_space.bold().color(self.color_space.col()),
i = "I".blue().bold(),
back_f = "image parser".magenta().bold(),
th_f = "threshold".magenta().bold(),
palette_f = "scheme palette".magenta().bold(),
cs_f = "colorspace".magenta().bold(),
);
}
pub fn write_entry(&self, wal_str: &WalStr, colors: &Colors, quiet: bool) -> Result<()> {
let init = format!("[{info}] {t}: ", info = "I".blue().bold(), t = "templates".magenta().bold());
let templates_header = match &self.templates {
Some(s) => {
if ! quiet { println!("{init}Writing templates.."); }
s
},
None => {
if ! quiet { println!("{init}No templates found"); }
return Ok(())
},
};
let image_path = match wal_str {
WalStr::Theme(s) => s.to_string(),
WalStr::Path(p) => dunce::canonicalize(p).expect("PATH EXIST, validation from clap").display().to_string(),
};
let values = TemplateFields {
alpha: self.alpha.unwrap_or(100),
backend: &self.backend,
colorspace: &self.color_space,
palette: &self.palette,
image_path: &image_path,
colors,
};
template::write_template(&self.templates_dir, templates_header, &values, quiet, self.env_vars.unwrap_or_default())
}
pub fn customs_cli(&mut self, cli: &WallustArgs) {
if let Some(b) = cli.backend {
self.backend = b;
}
if let Some(col) = cli.colorspace {
self.color_space = col;
}
if let Some(f) = cli.palette {
self.palette = f;
}
if let Some(t) = cli.threshold {
self.threshold = Some(t as u8); }
if let Some(a) = cli.alpha {
self.alpha = Some(a as u8);
}
if cli.check_contrast {
self.check_contrast = Some(cli.check_contrast);
}
if let Some(sat) = cli.saturation {
self.saturation = Some(sat as u8);
}
if let Some(g) = cli.fallback_generator {
self.fallback_generator = Some(g);
}
}
pub fn run_hooks(&self, quiet: bool) {
let hooks = match &self.hooks {
None => return,
Some(s) => s,
};
let init = format!("[{info}] {t}: ", info = "I".blue().bold(), t = "hooks".blue().bold());
if !quiet { println!("{init}Running hooks.."); }
for (k, v) in hooks {
match Command::new("sh").arg("-c").arg(v).spawn() {
Ok(_) => println!("{}: {}", k.italic(), "ok!".green().bold()),
Err(e) => eprintln!("{}: Couldn't run '{k}': {e}", k.italic().red()),
}
}
}
pub fn threshold_col(&self) -> AnsiColors {
match self.true_th {
1 => AnsiColors::Yellow,
2 => AnsiColors::Cyan,
3..=10 => AnsiColors::Green,
11..=49 => AnsiColors::Blue,
50..=100 => AnsiColors::Red,
_ => AnsiColors::Red,
}
}
}
use std::process::Command;
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let sp = " ";
let temps = if let Some(e) = &self.templates {
let mut s = String::new();
for i in e {
let pywal = if let Some(s) = i.1.pywal {
format!("{sp}{sp}pywal = {s}\n")
} else {
String::new()
};
let name = i.0;
s.push_str(
&format!("{sp}{name}\n{sp}{sp}template = {}\n{sp}{sp}target = {}\n{pywal}",
i.1.template, i.1.target)
);
}
s.trim_end().to_owned()
} else {
String::new()
};
let templates = if temps.is_empty() {
"No entries found.".into()
} else {
temps
};
write!(f, "\
Config directory: {dir}
Config file: {file}
Configuration options:
backend = {b}
color_space = {c}
threshold = {t:?}
palette = {f}
check_contrast = {con:?}
saturation = {sat:?}
alpha = {a:?}
Templates:
{templates}",
b = self.backend,
c = self.color_space,
t = self.threshold,
f = self.palette,
con = self.check_contrast,
sat = self.saturation,
a = self.alpha,
dir = self.dir.display(),
file = self.file.display(),
)
}
}
fn validate_threshold<'de, D>(d: D) -> Result<Option<u8>, D::Error>
where D: serde::de::Deserializer<'de>
{
use serde::de;
let value = Option::deserialize(d)?;
let value = match value {
Some(s) => s,
None => return Ok(None),
};
if value <= 100 { return Ok(Some(value)); }
Err(de::Error::invalid_value(de::Unexpected::Unsigned(value as u64), &"a value between 0 and 100."))
}
impl From<PrettyConfig> for Config {
fn from(value: PrettyConfig) -> Self {
Self {
alpha: value.alpha,
threshold: value.threshold,
backend_user: value.backend,
color_space_user: value.color_space,
palette_user: value.palette,
fallback_generator: value.fallback_generator,
check_contrast: value.check_contrast,
saturation: value.saturation,
templates: value.templates,
env_vars: value.env_vars,
hooks: value.hooks,
no_hooks: value.no_hooks.unwrap_or_default(),
..Self::default()
}
}
}