use crate::{
config::Args,
errors::{ConfigFileErrors, Errors},
time_track::Time,
};
use chrono::{Local, Timelike, Utc};
use clokwerk::{Scheduler, TimeUnits};
use std::{env, error::Error, process, process::Command, sync::Arc, thread::sleep, time::Duration};
use walkdir::{IntoIter, WalkDir};
use run_script::ScriptOptions;
use unicase::UniCase;
#[cfg(not(windows))]
use std::env::consts::ARCH;
#[cfg(windows)]
use std::ffi::OsStr;
#[cfg(windows)]
use std::{io, iter, os::raw::c_void, os::windows::ffi::OsStrExt};
#[cfg(windows)]
use winapi::um::winuser::{
SystemParametersInfoW, SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, SPI_SETDESKWALLPAPER,
};
pub mod config;
pub mod errors;
pub mod time_track;
const FULL_DAY: Time = Time {
hours: 24,
mins: 0,
total_mins: 1440,
};
const MIDNIGHT: Time = Time {
hours: 0,
mins: 0,
total_mins: 0,
};
pub fn wallpaper_current_time(
dir: &str,
progs: Arc<Option<Vec<String>>>,
times: &[Time],
backend: Arc<Option<String>>,
min_depth: usize,
) -> Result<(), Box<dyn Error>> {
let dir_iter = sorted_dir_iter(dir, min_depth);
let dir_count = sorted_dir_iter(dir, min_depth);
let dir_count: usize = dir_count.count();
let mut commands_vec: Vec<Command> = vec![];
let mut times_iter = times.iter();
let curr_time = Time::new(Local::now().hour() * 60 + Local::now().minute());
let loop_time = times_iter.next();
let mut next_time = times_iter.next().unwrap_or(&FULL_DAY);
let mut filepath_set: String = String::new();
let mut last_image = String::new();
let first_time = times[0];
let mut loop_time = error_checking(times, loop_time, dir_count)?;
for file in dir_iter {
if loop_time > *next_time {
loop_time = MIDNIGHT;
}
let filepath_temp = file.map_err(|_| Errors::FilePathError)?;
let filepath_temp = filepath_temp.path();
let last_image_temp = filepath_temp.to_str().unwrap();
last_image = last_image_temp.to_owned();
if curr_time >= loop_time && curr_time < *next_time {
filepath_set.push_str(match filepath_temp.to_str() {
Some(filepath) => Ok(filepath),
None => Err(Errors::FilePathError),
}?);
commands_vec_loader(&filepath_set, Arc::clone(&progs), &mut commands_vec);
}
loop_time = *next_time;
next_time = times_iter.next().unwrap_or(&first_time);
}
if filepath_set.is_empty() {
de_command_spawn(&last_image, backend)?;
commands_vec_loader(&last_image, Arc::clone(&progs), &mut commands_vec);
filepath_set = last_image;
} else {
de_command_spawn(&filepath_set, backend)?;
}
if let Some(progs) = progs.as_deref() {
let mut prog_iter = progs.iter();
for curr_command in commands_vec.iter_mut() {
curr_command
.spawn()
.map_err(|_| Errors::ProgramRunError(String::from(prog_iter.next().unwrap())))?;
println!(
"The image {} has been sent as an argument to the specified program",
filepath_set
);
}
}
Ok(())
}
pub fn wallpaper_listener(
dir: String,
args: Args,
min_depth: usize,
) -> Result<(), Box<dyn Error>> {
let mut scheduler = Scheduler::new();
let mut sched_addto = scheduler.every(1.day()).at("0:00");
let progs = Arc::new(args.programs);
let backend = Arc::new(args.backend);
let times = args.times.unwrap();
wallpaper_current_time(
&dir,
Arc::clone(&progs),
×,
Arc::clone(&backend),
min_depth,
)?;
for time in × {
let time_fmt = format!("{:02}:{:02}", time.hours, time.mins);
sched_addto = sched_addto.and_every(1.day()).at(time_fmt.as_str());
}
let sched_closure = move || {
let result = wallpaper_current_time(
&dir,
Arc::clone(&progs),
×,
Arc::clone(&backend),
min_depth,
);
match result {
Ok(s) => s,
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
}
};
sched_addto.run(sched_closure);
loop {
scheduler.run_pending();
sleep(Duration::from_millis(1000));
}
}
fn commands_vec_loader(
filepath_set: &str,
progs: Arc<Option<Vec<String>>>,
commands_vec: &mut Vec<Command>,
) {
if let Some(prog_vec) = progs.as_deref() {
for prog_str in prog_vec.iter() {
let mut wall_sent = false;
let mut prog_split = prog_str.split_whitespace();
let mut curr_command = Command::new(prog_split.next().unwrap());
for word in prog_split {
if word == "!WALL" {
curr_command.arg(filepath_set);
wall_sent = true;
} else {
curr_command.arg(word);
}
}
if wall_sent == false {
curr_command.arg(filepath_set);
}
commands_vec.push(curr_command);
}
}
}
pub fn auto_time_setup(dir: &str) -> (Result<Time, Errors>, Time) {
let dir_count = WalkDir::new(dir).into_iter().count() - 1;
let step_time = if dir_count == 0 {
Err(Errors::NoFilesFoundError(dir.to_string()))
} else {
Ok(Time::new(((24.0 / dir_count as f32) * 60.0) as u32))
};
let loop_time = Time::default();
(step_time, loop_time)
}
pub fn print_schedule(dir: &str, min_depth: usize, args: Args) -> Result<(), Box<dyn Error>> {
let mut dir_iter = sorted_dir_iter(dir, min_depth);
let dir_count = sorted_dir_iter(dir, min_depth).count();
let mut sched_str: Vec<String> = vec![];
let times = args.times.unwrap();
let mut times_iter = times.iter();
error_checking(×, times_iter.next(), dir_count)?;
for time in times_iter {
let file = dir_iter
.next()
.ok_or(Errors::ConfigFileError(ConfigFileErrors::FileTimeMismatch))??;
let file = file.file_name();
sched_str.push(format!("Image: {:?} Time: {}", file, time.twelve_hour()));
}
for line in sched_str.iter() {
println!("{}", line);
}
Ok(())
}
pub fn sorted_dir_iter(dir: &str, min_depth: usize) -> IntoIter {
WalkDir::new(dir)
.sort_by(|a, b| {
alphanumeric_sort::compare_str(
a.path().to_str().expect("Sorting directory files failed"),
b.path().to_str().expect("Sorting directory files failed"),
)
})
.min_depth(min_depth)
.into_iter()
}
fn error_checking(
times: &[Time],
loop_time: Option<&Time>,
dir_count: usize,
) -> Result<Time, Box<dyn Error>> {
let times_iter_err = times.iter();
let start_range = times
.iter()
.next()
.ok_or(Errors::ConfigFileError(ConfigFileErrors::Empty))?;
let mut start_range_other = times.iter().next().unwrap();
let mut curr_range = start_range.to_owned();
let mut curr_range_other = start_range.to_owned();
let mut other_inited = false;
let mut checked = vec![];
for time in times_iter_err {
if *time > *start_range && *time > curr_range {
curr_range = *time;
} else if *time > *start_range && *time < curr_range {
return Err(Errors::ConfigFileError(ConfigFileErrors::OutOfOrder).into());
} else if *time < *start_range {
if !other_inited {
curr_range = FULL_DAY;
start_range_other = time;
curr_range_other = *time;
other_inited = true
} else if *time > *start_range_other && *time > curr_range_other {
curr_range_other = *time;
} else {
return Err(Errors::ConfigFileError(ConfigFileErrors::OutOfOrder).into());
}
}
if time.total_mins >= 24 * 60 {
return Err(Errors::ConfigFileError(ConfigFileErrors::OutOfRange).into());
}
if checked.contains(time) {
return Err(Errors::ConfigFileError(ConfigFileErrors::DuplicatesFound).into());
}
checked.push(*time);
}
if times.len() != dir_count {
return Err(Errors::ConfigFileError(ConfigFileErrors::FileTimeMismatch).into());
}
let loop_time = match loop_time {
None => Err(Errors::ConfigFileError(ConfigFileErrors::Empty)),
Some(time) => Ok(time),
}?;
Ok(*loop_time)
}
#[cfg(windows)]
fn de_command_spawn(
filepath_set: &str,
backend: Arc<Option<String>>,
) -> Result<(), Box<dyn Error>> {
if backend.is_some() {
eprintln!("NOTE: You are unable to select a backend on windows");
}
unsafe {
let file = OsStr::new(filepath_set)
.encode_wide()
.chain(iter::once(0))
.collect::<Vec<u16>>();
let successful = SystemParametersInfoW(
SPI_SETDESKWALLPAPER,
0,
file.as_ptr() as *mut c_void,
SPIF_UPDATEINIFILE | SPIF_SENDCHANGE,
) == 1;
if successful {
println!("{} has been set as your wallpaper", filepath_set);
Ok(())
} else {
Err(io::Error::last_os_error().into())
}
}
}
#[cfg(not(windows))]
fn de_command_spawn(
filepath_set: &str,
backend: Arc<Option<String>>,
) -> Result<(), Box<dyn Error>> {
let backend = backend.as_deref();
let gnome = vec![
UniCase::new("gnome"),
UniCase::new("gnome-xorg"),
UniCase::new("ubuntu"),
UniCase::new("deepin"),
UniCase::new("pop"),
UniCase::new("ubuntu:gnome"),
];
let pantheon = UniCase::new("pantheon");
let mate = UniCase::new("mate");
let kde = vec![
UniCase::new("plasma"),
UniCase::new("neon"),
UniCase::new("kde"),
UniCase::new("/usr/share/xsessions/plasma"),
];
let lxde = UniCase::new("lxde");
let xfce = vec![
UniCase::new("xfce"),
UniCase::new("xubuntu"),
UniCase::new("xfce session"),
];
let curr_de = env::var("XDG_CURRENT_DESKTOP");
let curr_de = match curr_de {
Err(_) => String::from("Other"),
Ok(de) => de,
};
let mut curr_de = UniCase::new(curr_de.as_str());
let mut feh_handle = Command::new("feh");
let feh_handle = feh_handle.arg("--bg-scale").arg(filepath_set);
let mut gnome_handle = Command::new("gsettings");
let gnome_handle = gnome_handle
.arg("set")
.arg("org.gnome.desktop.background")
.arg("picture-uri")
.arg(format!("'file://{}'", filepath_set));
let mut multiarch_dir = String::from("/usr/lib/");
multiarch_dir.push_str(ARCH);
multiarch_dir.push_str("-linux-gnu/");
let mut pantheon_handle = Command::new(multiarch_dir + "io.elementary.contract.set-wallpaper");
let pantheon_handle = pantheon_handle
.arg(format!("{}", filepath_set));
let kde_script_beg = r#"
qdbus org.kde.plasmashell /PlasmaShell org.kde.PlasmaShell.evaluateScript "
var allDesktops = desktops();
print (allDesktops);
for (i=0;i<allDesktops.length;i++) {
d = allDesktops[i];
d.wallpaperPlugin = 'org.kde.image';
d.currentConfigGroup = Array('Wallpaper',
'org.kde.image',
'General');
d.writeConfig('Image', 'file://"#;
let kde_script_end = r#"')
}""#;
let kde_script = format!("{}{}{}", kde_script_beg, filepath_set, kde_script_end);
let mut lxde_handle = Command::new("pcmanfm");
let lxde_handle = lxde_handle.arg("--set-wallpaper").arg(filepath_set);
let mut mate_handle = Command::new("gsettings set org.mate.background picture-filename");
let mate_handle = mate_handle
.arg("set")
.arg("org.mate.background")
.arg("picture-uri")
.arg(format!("'file://{}'", filepath_set));
let xfce_script_beg = "xfconf-query -c xfce4-desktop -l | grep last-image | while read path; do xfconf-query -c xfce4-desktop -p $path -s ";
let xfce_script_end = r#"; done"#;
let xfce_script = format!("{}{}{}", xfce_script_beg, filepath_set, xfce_script_end);
let mut backend_split: Vec<&str> = vec![];
let mut cust_backend = false;
if let Some(back) = backend {
curr_de = UniCase::new(back);
cust_backend = true;
for word in back.split_whitespace() {
backend_split.push(word);
}
}
if gnome.contains(&curr_de) {
gnome_handle
.spawn()
.map_err(|_| Errors::ProgramRunError(String::from("Gnome Wallpaper Adjuster")))?;
} else if lxde == curr_de {
lxde_handle
.spawn()
.map_err(|_| Errors::ProgramRunError(String::from("LXDE Wallpaper Adjuster")))?;
} else if pantheon == curr_de {
pantheon_handle
.spawn()
.map_err(|_| Errors::ProgramRunError(String::from("Pantheon Wallpaper Adjuster")))?;
} else if mate == curr_de {
mate_handle
.spawn()
.map_err(|_| Errors::ProgramRunError(String::from("Mate Wallpaper Adjuster")))?;
} else if kde.contains(&curr_de) {
run_script::run(kde_script.as_str(), &vec![], &ScriptOptions::new())
.map_err(|_| Errors::ProgramRunError(String::from("KDE Wallpaper Adjuster")))?;
} else if xfce.contains(&curr_de) {
run_script::run(xfce_script.as_str(), &vec![], &ScriptOptions::new())
.map_err(|_| Errors::ProgramRunError(String::from("XFCE Wallpaper Adjuster")))?;
} else if !cust_backend || curr_de == UniCase::new("feh") {
feh_handle
.spawn()
.map_err(|_| Errors::ProgramRunError(String::from("Feh")))?;
} else if cust_backend {
let mut backend_split = backend_split.into_iter();
let mut cust_handle = Command::new(backend_split.next().unwrap());
let mut wall_sent = false;
for word in backend_split {
if word == "!WALL" {
wall_sent = true;
cust_handle.arg(filepath_set);
} else {
cust_handle.arg(word);
}
}
if !wall_sent {
cust_handle.arg(filepath_set);
}
cust_handle
.spawn()
.map_err(|_| Errors::ProgramRunError(curr_de.to_string()))?;
} else {
return Err(Errors::BackendNotFoundError(curr_de.to_string()).into());
}
println!("{} has been set as your wallpaper", filepath_set);
Ok(())
}
pub fn sun_timings(
dir: &str,
lat: f64,
long: f64,
elevation: f64,
) -> Result<Vec<Time>, Box<dyn Error>> {
let dir_night = format!("{}/night", dir);
let dir_night = dir_night.as_str();
let dir_day = format!("{}/day", dir);
let dir_day = dir_day.as_str();
let (dir_count_day, dir_count_night) = sun_timings_dir_counts(dir, dir_day, dir_night)?;
if dir_count_day == 0 {
return Err(Errors::NoFilesFoundError(String::from(dir_day)).into());
}
else if dir_count_night == 0 {
return Err(Errors::NoFilesFoundError(String::from(dir_night)).into());
}
let mut times: Vec<Time> = vec![];
let (sunrise, sunset) = sun_times::sun_times(Utc::today(), lat, long, elevation);
let (sunset, sunrise) = (sunset.with_timezone(&Local), sunrise.with_timezone(&Local));
let sunset = Time::new((sunset.hour() * 60) + sunset.minute());
let sunrise = Time::new((sunrise.hour() * 60) + sunrise.minute());
let step_time_day = Time::new((sunset.total_mins - sunrise.total_mins) / (dir_count_day));
let step_time_night =
Time::new((1440 - (sunset.total_mins - sunrise.total_mins)) / dir_count_night);
let mut loop_time_night: Time;
let mut loop_time_day = sunrise.to_owned();
while loop_time_day <= sunset {
if loop_time_day >= FULL_DAY {
times.push(loop_time_day - FULL_DAY);
} else {
times.push(loop_time_day);
}
loop_time_day += step_time_day;
}
times.pop();
loop_time_night = sunset.to_owned();
while loop_time_night < (sunrise + FULL_DAY) {
if loop_time_night >= FULL_DAY {
times.push(loop_time_night - FULL_DAY);
} else {
times.push(loop_time_night);
}
loop_time_night += step_time_night;
}
if i32::abs(sunrise.total_mins as i32 - times[times.len() - 1].total_mins as i32) < step_time_night.total_mins as i32 {
times.pop();
}
Ok(times)
}
fn sun_timings_dir_counts(
dir: &str,
dir_day: &str,
dir_night: &str,
) -> Result<(u32, u32), Box<dyn Error>> {
if check_dir_exists(dir).is_err() {
Err(Errors::FilePathError.into())
} else if check_dir_exists(dir_night).is_err() || check_dir_exists(dir_day).is_err() {
Err("Error: Make sure night and day directories are created within master directory".into())
} else {
let dir_count_night = WalkDir::new(dir_night).min_depth(1).into_iter().count();
let dir_count_day = WalkDir::new(dir_day).min_depth(1).into_iter().count();
Ok((dir_count_day as u32, dir_count_night as u32))
}
}
pub fn check_dir_exists(dir: &str) -> Result<(), Errors> {
let mut dir_iter = WalkDir::new(dir).into_iter();
if dir_iter.next().unwrap().is_err() {
Err(Errors::DirNonExistantError(dir.to_string()))
} else {
Ok(())
}
}