use serde::{Deserialize, Serialize};
use std::{
fmt::Debug,
str::{self, FromStr},
};
use crate::{
ENVIRON, Orientation,
WallSwitchError::{self, *},
WallSwitchResult, get_config_path, read_config_file,
};
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct Arguments {
pub min_size: Option<u64>,
pub max_size: Option<u64>,
pub config: bool,
pub min_dimension: Option<u64>,
pub max_dimension: Option<u64>,
pub help: bool,
pub interval: Option<u64>,
pub monitor: Option<u8>,
pub monitor_orientation: Option<Orientation>,
pub once: bool,
pub pictures_per_monitor: Option<u8>,
pub sort: bool,
pub verbose: bool,
pub version: bool,
}
impl Arguments {
pub fn build() -> WallSwitchResult<Arguments> {
let args = Arguments::parse(std::env::args())?;
if args.config {
let config_path = get_config_path()?;
let config = read_config_file(&config_path)?;
let json: String = serde_json::to_string_pretty(&config)?;
println!("{json}");
std::process::exit(0);
}
Ok(args)
}
fn parse(args: impl Iterator<Item = String>) -> WallSwitchResult<Self> {
let mut arguments = Arguments::default();
let args: Vec<String> = get_formatted_args(args);
if args.is_empty() {
return Ok(arguments);
}
let mut iter = args.into_iter();
while let Some(current) = iter.next() {
match current.as_ref() {
"--min_size" | "-b" => {
let min_size: u64 = parse_value(iter.next(), "--min_size", 0)?;
arguments.min_size = Some(min_size)
}
"--max_size" | "-B" => {
let max_size: u64 = parse_value(iter.next(), "--max_size", 0)?;
arguments.max_size = Some(max_size)
}
"--min_dimension" | "-d" => {
let dimension: u64 = parse_value(iter.next(), "--min_dimension", 10)?;
arguments.min_dimension = Some(dimension)
}
"--max_dimension" | "-D" => {
let dimension: u64 = parse_value(iter.next(), "--max_dimension", 0)?;
arguments.max_dimension = Some(dimension)
}
"--interval" | "-i" => {
let interval: u64 = parse_value(iter.next(), "--interval", 5)?;
arguments.interval = Some(interval)
}
"--monitor" | "-m" => {
let value: u64 = parse_value(iter.next(), "--monitor", 1)?;
let monitor: u8 = value.try_into().map_err(WallSwitchError::from)?;
arguments.monitor = Some(monitor)
}
"--orientation" | "-o" => {
let orientation = parse_orientation(iter.next(), "--orientation")?;
arguments.monitor_orientation = Some(orientation)
}
"--pictures_per_monitor" | "-p" => {
let value: u64 = parse_value(iter.next(), "--pictures_per_monitor", 1)?;
let pictures_per_monitor: u8 =
value.try_into().map_err(WallSwitchError::from)?;
arguments.pictures_per_monitor = Some(pictures_per_monitor)
}
"--config" | "-c" => arguments.config = true,
"--help" | "-h" => show_help_summary(),
"--sort" | "-s" => arguments.sort = true,
"--once" => arguments.once = true,
"--verbose" | "-v" => arguments.verbose = true,
"--Version" | "-V" => show_version(),
_ => return Err(UnexpectedArg { arg: current }),
}
}
Ok(arguments)
}
}
fn get_formatted_args(args: impl Iterator<Item = String>) -> Vec<String> {
args.skip(1) .flat_map(|arg: String| {
arg.split('=')
.map(ToString::to_string)
.collect::<Vec<String>>()
})
.flat_map(|arg: String| {
if let Some(index) = arg.find(|c: char| c.is_ascii_digit()) {
vec![arg[..index].to_string(), arg[index..].to_string()]
} else {
vec![arg]
}
})
.map(|arg: String| arg.trim().to_string())
.filter(|arg| !arg.is_empty())
.collect()
}
fn parse_value(opt_value: Option<String>, name: &'static str, min: u64) -> WallSwitchResult<u64> {
if let Some(value) = opt_value {
match value.parse::<u64>() {
Ok(num) if num >= min => Ok(num),
Ok(_) => Err(AtLeastValue {
arg: name.to_string(),
value,
num: min,
}),
Err(_) => Err(InvalidValue {
arg: name.to_string(),
value,
}),
}
} else {
Err(MissingValue {
arg: name.to_string(),
})
}
}
fn parse_orientation(
opt_string: Option<String>,
name: &'static str,
) -> WallSwitchResult<Orientation> {
if let Some(string) = opt_string {
Orientation::from_str(&string)
} else {
Err(MissingValue {
arg: name.to_string(),
})
}
}
fn show_help_summary() {
let pkg_name = ENVIRON.get_pkg_name();
let pkg_descr = env!("CARGO_PKG_DESCRIPTION");
println!("{pkg_descr}");
println!("\nUsage: {pkg_name} [OPTIONS]\n");
println!("Options:\n");
println!(
"-b, --min_size <MIN_SIZE>\n\tSet a minimum file size (in bytes) for searching image files"
);
println!(
"-B, --max_size <MAX_SIZE>\n\tSet a maximum file size (in bytes) for searching image files"
);
println!("-c, --config\n\tRead the configuration file and exit the program");
println!(
"-d, --min_dimension <MIN_DIMENSION>\n\tSet the minimum dimension that the height and width must satisfy"
);
println!(
"-D, --max_dimension <MAX_DIMENSION>\n\tSet the maximum dimension that the height and width must satisfy"
);
println!("-h, --help\n\tPrint help");
println!(
"-i, --interval <INTERVAL>\n\tSet the interval (in seconds) between each wallpaper displayed"
);
println!("-m, --monitor <MONITOR_NUMBER>\n\tSet the number of monitors [default: 2]");
println!(
"-o, --orientation <ORIENTATION>\n\tInform monitor orientation: Horizontal (side-by-side) or Vertical (stacked)."
);
println!("--once\n\tRun a single wallpaper update cycle and exit");
println!(
"-p, --pictures_per_monitor <PICTURE>\n\tSet number of pictures (or images) per monitor [default: 1]"
);
println!("-s, --sort\n\tSort the images found");
println!("-v, --verbose\n\tShow intermediate runtime messages");
println!("-V, --version\n\tPrint version");
std::process::exit(0);
}
fn show_version() {
let pkg_name = ENVIRON.get_pkg_name();
let pkg_version = env!("CARGO_PKG_VERSION");
println!("{pkg_name} {pkg_version}");
std::process::exit(0);
}
#[cfg(test)]
mod test_args_v2 {
use crate::{Arguments, Orientation, WallSwitchResult};
#[test]
fn get_arguments() -> WallSwitchResult<()> {
let entries = [
"program_name",
"-i",
"60",
" -d === ",
"200",
"--config",
"--monitor=",
"3",
"-m==5",
"-c",
"-p",
"3",
"--orientation",
"horiZontal",
];
println!("entries: {entries:?}");
let args: Vec<String> = entries.iter().map(ToString::to_string).collect();
let arguments = Arguments::parse(args.into_iter())?;
println!("arguments: {arguments:#?}");
let json: String = serde_json::to_string_pretty(&arguments)?;
println!("arguments: {json}");
assert_eq!(
arguments,
Arguments {
config: true,
monitor_orientation: Some(Orientation::Horizontal),
min_dimension: Some(200),
max_dimension: None,
min_size: None,
max_size: None,
help: false,
interval: Some(60),
monitor: Some(5),
pictures_per_monitor: Some(3),
sort: false,
verbose: false,
version: false,
}
);
Ok(())
}
#[test]
fn split_arg_equal() -> WallSwitchResult<()> {
let arg = "Löwe 老虎 Léo=öpard Gepa12345虎==rdi".to_string();
println!("arg: {arg}");
let result1: Vec<String> = if let Some(index) = arg.find('=') {
vec![arg[..index].to_string(), arg[index + 1..].to_string()]
} else {
vec![arg.clone()]
};
let result2: Vec<&str> = arg
.split_once('=')
.into_iter()
.flat_map(|(a, b)| vec![a, b])
.collect();
println!("result1: {result1:?}");
assert_eq!(result1, ["Löwe 老虎 Léo", "öpard Gepa12345虎==rdi"]);
assert_eq!(result1, result2);
Ok(())
}
#[test]
fn split_arg_digit() -> WallSwitchResult<()> {
let arg = "Löwe 老虎 Léo=pard Gepa12345虎rdi".to_string();
println!("arg: {arg}");
let result = if let Some(index) = arg.find(|c: char| c.is_ascii_digit()) {
vec![arg[..index].to_string(), arg[index..].to_string()]
} else {
vec![arg]
};
println!("result: {result:?}");
assert_eq!(result, ["Löwe 老虎 Léo=pard Gepa", "12345虎rdi"]);
Ok(())
}
}