mod config; mod effects; use anyhow::{bail, Context, Result}; use std::io::Write as _; use std::process::{Command, Stdio}; #[derive(Default)] struct CliOverrides { rounded_corners: Option, corner_radius: Option, drop_shadow: Option, shadow_blur_radius: Option, shadow_blur_passes: Option, shadow_offset_x: Option, shadow_offset_y: Option, // Accepted as four comma-separated u8 values, e.g. `255,0,0,200` shadow_color: Option<[u8; 4]>, } fn parse_bool(s: &str) -> Result { match s.to_lowercase().as_str() { "true" | "1" | "yes" => Ok(true), "false" | "0" | "no" => Ok(false), other => bail!("Expected a boolean (true/false), got '{other}'"), } } fn parse_shadow_color(s: &str) -> Result<[u8; 4]> { let parts: Vec<&str> = s.split(',').collect(); if parts.len() != 4 { bail!("--shadow-color expects four comma-separated u8 values, e.g. 255,0,0,200"); } let r = parts[0] .trim() .parse::() .context("shadow-color red channel")?; let g = parts[1] .trim() .parse::() .context("shadow-color green channel")?; let b = parts[2] .trim() .parse::() .context("shadow-color blue channel")?; let a = parts[3] .trim() .parse::() .context("shadow-color alpha channel")?; Ok([r, g, b, a]) } fn main() -> Result<()> { let args: Vec = std::env::args().skip(1).collect(); let mut image_path: Option = None; let mut overrides = CliOverrides::default(); let mut scale: Option = None; let mut i = 0; while i < args.len() { match args[i].as_str() { "--image" => { i += 1; image_path = Some( args.get(i) .cloned() .context("Expected a path after --image")?, ); } "--rounded-corners" => { i += 1; let val = args .get(i) .context("Expected true/false after --rounded-corners")?; overrides.rounded_corners = Some(parse_bool(val)?); } "--corner-radius" => { i += 1; let val = args .get(i) .context("Expected a number after --corner-radius")?; overrides.corner_radius = Some( val.parse::() .context("--corner-radius must be a number")?, ); } "--drop-shadow" => { i += 1; let val = args .get(i) .context("Expected true/false after --drop-shadow")?; overrides.drop_shadow = Some(parse_bool(val)?); } "--shadow-blur-radius" => { i += 1; let val = args .get(i) .context("Expected a number after --shadow-blur-radius")?; overrides.shadow_blur_radius = Some( val.parse::() .context("--shadow-blur-radius must be a number")?, ); } "--shadow-offset-x" => { i += 1; let val = args .get(i) .context("Expected a number after --shadow-offset-x")?; overrides.shadow_offset_x = Some( val.parse::() .context("--shadow-offset-x must be a number")?, ); } "--shadow-offset-y" => { i += 1; let val = args .get(i) .context("Expected a number after --shadow-offset-y")?; overrides.shadow_offset_y = Some( val.parse::() .context("--shadow-offset-y must be a number")?, ); } "--shadow-blur-passes" => { i += 1; let val = args .get(i) .context("Expected a number after --shadow-blur-passes")?; overrides.shadow_blur_passes = Some( val.parse::() .context("--shadow-blur-passes must be a number")?, ); } "--shadow-color" => { i += 1; let val = args .get(i) .context("Expected r,g,b,a after --shadow-color")?; overrides.shadow_color = Some(parse_shadow_color(val)?); } "--scale" => { i += 1; let val = args.get(i).context("Expected a number after --scale")?; scale = Some(val.parse::().context("--scale must be a number")?); } unknown => bail!("Unknown argument: {unknown}"), } i += 1; } let image_path = image_path.context("Missing --image ")?; let config = config::Config::load().context("Failed to load config")?; let mut effects = config.screenshot; if effects.mode == "auto" { if let Some(v) = overrides.rounded_corners { effects.rounded_corners = v; } if let Some(v) = overrides.corner_radius { effects.corner_radius = v; } if let Some(v) = overrides.drop_shadow { effects.drop_shadow = v; } if let Some(v) = overrides.shadow_blur_radius { effects.shadow_blur_radius = v; } if let Some(v) = overrides.shadow_offset_x { effects.shadow_offset_x = v; } if let Some(v) = overrides.shadow_offset_y { effects.shadow_offset_y = v; } if let Some(v) = overrides.shadow_blur_passes { effects.shadow_blur_passes = v; } if let Some(v) = overrides.shadow_color { effects.shadow_color = v; } } // if scale is set do if let Some(scale) = scale.filter(|&s| s != 1.0) { effects.corner_radius *= scale; effects.shadow_blur_radius *= scale; effects.shadow_offset_x *= scale; effects.shadow_offset_y *= scale; } if let Err(e) = process_image(&image_path, &effects) { eprintln!("Error processing '{}': {e:#}", image_path); } Ok(()) } fn process_image(path: &str, effects: &config::EffectsConfig) -> Result<()> { let img = image::open(path) .with_context(|| format!("Failed to open image '{path}'"))? .into_rgba8(); let processed = effects::apply_effects(img, effects); let mut png_bytes: Vec = Vec::new(); image::DynamicImage::ImageRgba8(processed) .write_to( &mut std::io::Cursor::new(&mut png_bytes), image::ImageFormat::Png, ) .context("Failed to encode processed image as PNG")?; let mut child = Command::new("swappy") .args(["-f", "-"]) .stdin(Stdio::piped()) .spawn() .context("Failed to spawn swappy. Is it installed and in PATH?")?; // Writes the PNG bytes to swappy's stdin and then closes if let Some(mut stdin) = child.stdin.take() { stdin .write_all(&png_bytes) .context("Failed to write image data to swappy")?; } Ok(()) }