use anyhow::{Context, Result}; use directories::ProjectDirs; use serde::{Deserialize, Serialize}; use std::path::PathBuf; /// Top-level application configuration. /// Serialized to/from ~/.config/rs-pictures/config.toml #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// Directory where screenshots are saved. pub save_directory: PathBuf, /// File format for saved screenshots: "png" or "jpeg". pub save_format: String, /// Filename template. Supports strftime-style tokens via chrono. /// Example: "screenshot_%Y-%m-%d_%H-%M-%S" pub filename_template: String, /// When true, automatically save to disk after capture without opening /// the review window. #[serde(default)] pub auto_save: bool, /// When true, automatically copy to the clipboard after capture without /// opening the review window. #[serde(default)] pub auto_copy: bool, /// Milliseconds to wait after launch before capturing the desktop snapshot. /// Increase this if the overlay background still shows the terminal/launcher /// that started rs-pictures. Default: 200. #[serde(default = "default_capture_delay_ms")] pub capture_delay_ms: u64, /// If true, the selection overlay will be transparent (live preview) instead of /// a frozen screenshot. The final capture happens after selection. #[serde(default)] pub live_mode: bool, /// Visual effects applied after capture. pub effects: EffectsConfig, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EffectsConfig { /// Apply rounded corners to the screenshot. pub rounded_corners: bool, /// Radius in pixels for rounded corners. pub corner_radius: f32, /// Apply a drop shadow beneath the screenshot. pub drop_shadow: bool, /// Blur radius for the drop shadow (higher = softer shadow). pub shadow_blur_radius: f32, /// Horizontal offset of the shadow in pixels. pub shadow_offset_x: f32, /// Vertical offset of the shadow in pixels. pub shadow_offset_y: f32, /// Shadow color as [R, G, B, A] in 0..=255. pub shadow_color: [u8; 4], } impl Default for EffectsConfig { fn default() -> Self { Self { rounded_corners: false, corner_radius: 12.0, drop_shadow: false, shadow_blur_radius: 20.0, shadow_offset_x: 5.0, shadow_offset_y: 8.0, shadow_color: [0, 0, 0, 160], } } } impl Default for Config { fn default() -> Self { let save_directory = dirs_default_pictures().unwrap_or_else(|| PathBuf::from(".")); Self { save_directory, save_format: "png".into(), filename_template: "screenshot_%Y-%m-%d_%H-%M-%S".into(), auto_save: false, auto_copy: false, capture_delay_ms: default_capture_delay_ms(), live_mode: false, effects: EffectsConfig::default(), } } } impl Config { /// Returns the path to the config file, creating parent directories if needed. pub fn config_path() -> Option { ProjectDirs::from("", "", "rs-pictures") .map(|pd| pd.config_dir().join("config.toml")) } /// Load config from disk, or return the default config if the file doesn't exist. pub fn load() -> Result { let path = match Self::config_path() { Some(p) => p, None => return Ok(Self::default()), }; if !path.exists() { let config = Self::default(); config.save()?; return Ok(config); } let raw = std::fs::read_to_string(&path) .with_context(|| format!("Failed to read config at {}", path.display()))?; toml::from_str(&raw) .with_context(|| format!("Failed to parse config at {}", path.display())) } /// Persist the current config to disk. pub fn save(&self) -> Result<()> { let path = Self::config_path() .context("Could not determine config directory")?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) .with_context(|| format!("Failed to create config dir {}", parent.display()))?; } let serialized = toml::to_string_pretty(self) .context("Failed to serialize config")?; std::fs::write(&path, serialized) .with_context(|| format!("Failed to write config to {}", path.display()))?; Ok(()) } /// Build the full output path for a new screenshot using chrono formatting. pub fn output_path(&self) -> PathBuf { let now = chrono::Local::now(); let filename = now.format(&self.filename_template).to_string(); let ext = &self.save_format; self.save_directory.join(format!("{filename}.{ext}")) } } fn default_capture_delay_ms() -> u64 { 200 } fn dirs_default_pictures() -> Option { // Use XDG_PICTURES_DIR if available, otherwise ~/Pictures if let Ok(val) = std::env::var("XDG_PICTURES_DIR") { return Some(PathBuf::from(val)); } directories::UserDirs::new() .and_then(|ud| ud.picture_dir().map(|p| p.to_path_buf())) }