diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..615a364 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,33 @@ +use anyhow::{Result, bail}; +use pico_args::Arguments; +use std::path::PathBuf; + +/// Parsed command-line arguments. +#[derive(Debug)] +pub struct Args { + pub set: Option, + pub update: Option, + pub mode: Option, // parsed now, used later +} + +impl Args { + pub fn parse() -> Result { + let mut args = Arguments::from_env(); + + let set = args.opt_value_from_str("--set")?; + let update = args.opt_value_from_str("--update")?; + let mode = args.opt_value_from_str("--mode")?; + + if set.is_some() && update.is_some() { + bail!("--set and --update cannot be used together"); + } + + // Fail if unknown arguments are present + let remaining = args.finish(); + if !remaining.is_empty() { + bail!("Unknown arguments: {remaining:?}"); + } + + Ok(Self { set, update, mode }) + } +} diff --git a/src/config.rs b/src/config.rs index 9850cec..c211d28 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,10 +1,10 @@ use anyhow::{Context, Result}; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; /// Runtime configuration for `icing`. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Config { pub background_image: String, pub mode: Option, // Unused now, deal with it later @@ -23,6 +23,23 @@ impl Config { Ok(config) } + + /// Update config with image specifeid by CLI arg + pub fn update_background_image(&self, new_path: &str) -> Result<()> { + let path = config_path()?; + + let updated = toml::to_string(&Config { + background_image: new_path.to_string(), + mode: self.mode.clone(), + })?; + + fs::create_dir_all(path.parent().expect("config path has parent"))?; + + fs::write(&path, updated) + .with_context(|| format!("Failed to write config file: {}", path.display()))?; + + Ok(()) + } } fn config_path() -> Result { diff --git a/src/main.rs b/src/main.rs index d9ff495..2b6f913 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,13 +10,53 @@ use x11rb::{ }, rust_connection::RustConnection, }; +mod args; mod config; fn main() -> Result<()> { - // Load config - let config = config::Config::load()?; - // Path to wallpaper image in config - let image_path = &config.background_image; + // Parse CLI arguments + let args = args::Args::parse()?; + + // Load config file if it exists. Can be missing. + let config = config::Config::load().ok(); + + // Resolve wallpaper image path by precedence: + // 1. args (`--set`, `--update`): Use path from args. + // 2. config: Use path from config. + let image_path = match (&args.set, &args.update) { + // Case: arg `--set`: One-shot wallpaper change. Not persistent. + (Some(path), None) => path.clone(), + + // Case: arg `--update`. Persist wallpaper path to config (create if missing), then apply it. + (None, Some(path)) => { + let cfg = config.unwrap_or_else(|| config::Config { + background_image: String::new(), + mode: None, + }); + + // Write to config. + cfg.update_background_image( + path.to_str() + // Unsupported path format. + .ok_or_else(|| anyhow::anyhow!("Non-UTF8 path"))?, + )?; + path.clone() + } + + // Case: No args. Fallback to config file. + (None, None) => { + let cfg = config.ok_or_else(|| { + // No CLI args and no valid config. + anyhow::anyhow!("No or invalid image path specified in config or args.") + })?; + + // Successfully loaded. + cfg.background_image.into() + } + + // Case: Both args `--set` & `--update`. This case is already rejected during argument parsing. + _ => unreachable!(), + }; // Connect to the running graphical session, the X11 server. let (conn, screen_num) = RustConnection::connect(None)?; @@ -61,8 +101,8 @@ fn main() -> Result<()> { conn.create_pixmap(depth, pixmap, root, width, height)?; // Load the wallpaper image from disk. - let img = - image::open(image_path).with_context(|| format!("Failed to open image: {image_path}"))?; + let img = image::open(&image_path) + .with_context(|| format!("Failed to open image: {}", image_path.display()))?; // Convert the image into RGBA8 (4 bytes per pixel). // Predictable, CPU-friendly format to work on.