use crate::{
Arguments, Complex, Desktop, Environment, Monitor, Orientation, ProceduralEffect, U8Extension,
WallSwitchError, WallSwitchResult, get_feh_path, get_monitors,
};
use serde::{Deserialize, Serialize};
use std::{
fs::{self, File},
io::{BufReader, BufWriter, Write},
path::{Path, PathBuf},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EffectsConfig {
#[serde(default = "default_true")]
pub add_presets: bool,
#[serde(default = "default_min_iterations")]
pub min_iterations: u32,
#[serde(default = "default_max_iterations")]
pub max_iterations: u32,
#[serde(default)]
pub julia: Vec<CustomFractalPreset>,
#[serde(default)]
pub mandelbrot: Vec<CustomFractalPreset>,
#[serde(default)]
pub newton: Vec<CustomNewtonPreset>,
#[serde(default)]
pub nova: Vec<CustomNovaPreset>,
}
impl Default for EffectsConfig {
fn default() -> Self {
Self {
add_presets: default_true(),
min_iterations: default_min_iterations(),
max_iterations: default_max_iterations(),
julia: vec![
CustomFractalPreset {
center: Complex { re: -0.8, im: 0.18 },
fractal_name: "Stardust spiral galaxy arms".to_string(),
},
CustomFractalPreset {
center: Complex {
re: 0.285,
im: 0.535,
},
fractal_name: "Pinwheel orbital clouds".to_string(),
},
],
mandelbrot: vec![
CustomFractalPreset {
center: Complex {
re: -0.74,
im: 0.24,
},
fractal_name: "custom v1".to_string(),
},
CustomFractalPreset {
center: Complex {
re: -0.088,
im: 0.655,
},
fractal_name: "custom v2".to_string(),
},
],
newton: vec![
CustomNewtonPreset {
power: 7,
lambda: Complex { re: 0.95, im: 0.55 },
name: "Aetheric prismatic vortex".to_string(),
},
CustomNewtonPreset {
power: 3,
lambda: Complex { re: 1.50, im: 0.25 },
name: "Over-relaxed geometric crown".to_string(),
},
],
nova: vec![
CustomNovaPreset {
power: 4,
c: Complex {
re: -0.15,
im: -0.35,
},
r: Complex { re: 1.10, im: 0.20 },
name: "Bioluminescent plasma plumes".to_string(),
},
CustomNovaPreset {
power: 6,
c: Complex { re: 0.25, im: 0.40 },
r: Complex {
re: 0.85,
im: -0.15,
},
name: "Astral jellyfish lattice".to_string(),
},
],
}
}
}
fn default_true() -> bool {
true
}
fn default_min_iterations() -> u32 {
600
}
fn default_max_iterations() -> u32 {
1200
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomFractalPreset {
pub center: Complex,
pub fractal_name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomNewtonPreset {
pub power: u32,
pub lambda: Complex,
pub name: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CustomNovaPreset {
pub power: u32,
pub c: Complex,
pub r: Complex,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Config {
pub desktop: Desktop,
pub directories: Vec<PathBuf>,
pub extensions: Vec<String>,
pub interval: u64,
pub min_dimension: u64,
pub max_dimension: u64,
pub min_size: u64,
pub max_size: u64,
pub monitors: Vec<Monitor>,
pub monitor_orientation: Orientation,
#[serde(skip)]
pub once: bool,
pub path_feh: PathBuf,
pub sort: bool,
pub effect: ProceduralEffect,
#[serde(default)]
pub effects: EffectsConfig,
pub wallpaper: PathBuf,
#[serde(skip)]
pub dry_run: bool,
pub transition_type: String,
pub transition_duration: u16,
pub transition_fps: u16,
pub transition_angle: u16,
pub transition_pos: String,
#[serde(skip)]
pub verbose: bool,
}
impl Default for Config {
fn default() -> Self {
let extensions: Vec<String> = ["avif", "jpg", "jpeg", "png", "tif", "webp"]
.iter()
.map(ToString::to_string)
.collect();
let interval: u64 = 30 * 60;
let min_dimension: u64 = 600;
let max_dimension: u64 = 128_000;
Config {
desktop: Desktop::detect(),
min_dimension,
max_dimension,
min_size: u64::pow(1024, 1), max_size: u64::pow(1024, 3), directories: get_directories().unwrap_or_default(),
effect: ProceduralEffect::None,
effects: EffectsConfig::default(),
extensions,
interval,
monitors: get_monitors(2),
monitor_orientation: Orientation::Horizontal,
once: false,
path_feh: PathBuf::from("/usr/bin/feh"),
sort: false,
verbose: false,
wallpaper: get_wallpaper_path().unwrap_or_default(),
dry_run: false,
transition_type: "random".to_string(),
transition_duration: 2,
transition_fps: 60,
transition_angle: 45,
transition_pos: "center".to_string(),
}
}
}
fn config_boundary() -> Config {
Config {
interval: 5,
min_dimension: 10,
min_size: 1,
monitors: vec![Monitor::default()],
..Config::default()
}
}
impl Config {
pub fn new(args: &Arguments) -> WallSwitchResult<Self> {
let mut read_default_config = false;
let config_path: PathBuf = get_config_path()?;
let config: Config = match read_config_file(&config_path) {
Ok(configuration) => configuration,
Err(_) => {
read_default_config = true;
Self::default()
}
}
.set_command_line_arguments(args)?
.validate_config()?
.write_config_file(&config_path, read_default_config)?;
Ok(config)
}
pub fn in_range(&self, value: u64) -> bool {
self.min_dimension <= value && value <= self.max_dimension
}
pub fn print(&self) -> WallSwitchResult<()> {
let json: String = serde_json::to_string_pretty(self)?;
println!("Config:\n{json}\n");
Ok(())
}
fn set_command_line_arguments(mut self, args: &Arguments) -> WallSwitchResult<Self> {
if let Some(min_dimension) = args.min_dimension {
self.min_dimension = min_dimension;
}
if let Some(max_dimension) = args.max_dimension {
self.max_dimension = max_dimension;
}
if let Some(min_size) = args.min_size {
self.min_size = min_size;
}
if let Some(max_size) = args.max_size {
self.max_size = max_size;
}
if let Some(interval) = args.interval {
self.interval = interval;
}
if let Some(monitor) = args.monitor {
self.monitors = get_monitors(monitor.into());
}
if let Some(orientation) = &args.monitor_orientation {
self.monitor_orientation = orientation.clone();
}
if let Some(pictures_per_monitor) = args.pictures_per_monitor {
for monitor in &mut self.monitors {
monitor.pictures_per_monitor = pictures_per_monitor;
}
}
self.once = args.once;
if args.dry_run {
self.dry_run = true;
self.once = true; }
if let Some(ref t) = args.transition_type {
self.transition_type = t.clone();
}
if let Some(d) = args.transition_duration {
self.transition_duration = d;
}
if let Some(f) = args.transition_fps {
self.transition_fps = f;
}
if let Some(a) = args.transition_angle {
self.transition_angle = a;
}
if let Some(ref p) = args.transition_pos {
self.transition_pos = p.clone();
}
if let Some(effect) = args.effect {
self.effect = effect;
}
if let Some(effects_add_presets) = args.effects_add_presets {
self.effects.add_presets = effects_add_presets;
}
if let Some(effects_min_iterations) = args.effects_min_iterations {
self.effects.min_iterations = effects_min_iterations;
}
if let Some(effects_max_iterations) = args.effects_max_iterations {
self.effects.max_iterations = effects_max_iterations;
}
if args.sort {
self.sort = !self.sort;
}
if args.verbose {
self.verbose = !self.verbose;
}
self.desktop = Desktop::detect();
Ok(self)
}
pub fn validate_config(mut self) -> WallSwitchResult<Self> {
let boundary: Config = config_boundary();
if self.interval < boundary.interval {
let value = self.interval.to_string();
return Err(WallSwitchError::AtLeastValue {
arg: "--interval".to_string(),
value,
num: boundary.interval,
});
}
if !self.path_feh.is_file() {
self.path_feh = get_feh_path(true)?;
}
if self.min_dimension < boundary.min_dimension {
let value = self.min_dimension.to_string();
return Err(WallSwitchError::AtLeastValue {
arg: "--min_dimension".to_string(),
value,
num: boundary.min_dimension,
});
}
if self.min_size < boundary.min_size {
let value = self.min_size.to_string();
return Err(WallSwitchError::AtLeastValue {
arg: "--min_size".to_string(),
value,
num: boundary.min_size,
});
}
if self.monitors.is_empty() {
let value = self.monitors.len().to_string();
return Err(WallSwitchError::AtLeastValue {
arg: "--interval".to_string(),
value,
num: 1,
});
}
for monitor in &self.monitors {
if monitor.pictures_per_monitor < 1 {
let value = monitor.pictures_per_monitor.to_string();
return Err(WallSwitchError::AtLeastValue {
arg: "--picture".to_string(),
value,
num: 1,
});
}
}
if let Some(parent) = self.wallpaper.parent()
&& !parent.exists()
{
fs::create_dir_all(parent)?;
}
for (min, max) in [
(self.min_dimension, self.max_dimension),
(self.min_size, self.max_size),
] {
if min > max {
return Err(WallSwitchError::MinMax { min, max });
}
}
if self.effects.min_iterations > self.effects.max_iterations {
return Err(WallSwitchError::MinMax {
min: self.effects.min_iterations as u64,
max: self.effects.max_iterations as u64,
});
}
Ok(self)
}
pub fn write_config_file(
self,
path: &PathBuf,
read_default_config: bool,
) -> WallSwitchResult<Self> {
if read_default_config {
eprintln!("Create the configuration file: {path:?}\n");
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?
};
let file: File = fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(true)
.open(path)
.map_err(|io_error| WallSwitchError::IOError {
path: path.to_path_buf(),
io_error,
})?;
let mut writer = BufWriter::new(file);
serde_json::to_writer_pretty(&mut writer, &self)?;
writer.flush()?;
Ok(self)
}
pub fn get_number_of_images(&self) -> usize {
self.monitors
.iter()
.map(|monitor| monitor.pictures_per_monitor.to_usize())
.sum()
}
}
pub fn get_wallpaper_path() -> WallSwitchResult<PathBuf> {
let env = Environment::new()?;
let home = env.get_home();
let pkg_name = env.get_pkg_name();
let mut wallpaper_path: PathBuf = [home, pkg_name].iter().collect();
wallpaper_path.set_extension("jpg");
Ok(wallpaper_path)
}
pub fn get_directories() -> WallSwitchResult<Vec<PathBuf>> {
let env = Environment::new()?;
let home = env.get_home();
let images = ["Figures", "Images", "Pictures", "Wallpapers", "Imagens"];
let directories_home: Vec<PathBuf> = images
.into_iter()
.map(|image| Path::new(home).join(image))
.collect();
let sep = std::path::MAIN_SEPARATOR.to_string();
let path1: PathBuf = [&sep, "usr", "share", "wallpapers"].iter().collect();
let path2: PathBuf = [&sep, "usr", "share", "backgrounds"].iter().collect();
let directories_others: Vec<PathBuf> = vec![path1, path2];
Ok(directories_home
.into_iter()
.chain(directories_others)
.collect())
}
pub fn get_config_path() -> WallSwitchResult<PathBuf> {
let env = Environment::new()?;
let home = env.get_home();
let pkg_name = env.get_pkg_name();
let hidden_dir = ".config";
let mut config_path: PathBuf = [home, hidden_dir, pkg_name, pkg_name].iter().collect();
config_path.set_extension("json");
Ok(config_path)
}
pub fn read_config_file<P>(path: P) -> WallSwitchResult<Config>
where
P: AsRef<Path>,
{
let file = File::open(path)?;
let reader = BufReader::new(file);
let config: Config = serde_json::from_reader(reader)?;
Ok(config)
}