Skip to main content

wallr_core/wallpaper/
mod.rs

1use crate::cache::CacheManager;
2use crate::config::{ThemeProvider, WallrConfig};
3use crate::packages::PackageRegistry;
4use crate::theme;
5use anyhow::Result;
6use std::path::Path;
7use tracing::info;
8
9#[derive(Debug, Clone)]
10pub struct SetOptions {
11    pub no_theme: bool,
12    /// Per-invocation theme provider override (wins over config.theme.provider).
13    pub theme_provider: Option<ThemeProvider>,
14    pub monitor: Option<String>,
15}
16
17#[derive(Debug, Clone)]
18pub enum DiagnosticStatus {
19    Pass,
20    Warn,
21    Fail,
22}
23
24#[derive(Debug, Clone)]
25pub struct DiagnosticCheck {
26    pub name: String,
27    pub status: DiagnosticStatus,
28    pub message: String,
29}
30
31#[derive(Debug, Clone)]
32pub struct DiagnosticReport {
33    pub checks: Vec<DiagnosticCheck>,
34}
35
36#[derive(Debug, Clone)]
37pub struct ValidationCheck {
38    pub name: String,
39    pub passed: bool,
40    pub message: Option<String>,
41}
42
43#[derive(Debug, Clone)]
44pub struct ValidationReport {
45    pub checks: Vec<ValidationCheck>,
46}
47
48#[derive(Debug, thiserror::Error)]
49pub enum WallpaperError {
50    #[error("Theme error: {0}")]
51    Theme(#[from] crate::theme::ThemeError),
52    #[error("Cache error: {0}")]
53    Cache(#[from] crate::cache::CacheError),
54    #[error("Package error: {0}")]
55    Package(#[from] crate::packages::PackageError),
56    #[error("Custom error: {0}")]
57    Custom(String),
58}
59
60pub struct WallpaperEngine {
61    pub config: WallrConfig,
62    pub cache: CacheManager,
63    pub registry: PackageRegistry,
64}
65
66impl WallpaperEngine {
67    pub fn new(config: WallrConfig) -> Result<Self, WallpaperError> {
68        let cache = CacheManager::new(&config.cache)?;
69        let registry = PackageRegistry::new()?;
70        Ok(Self {
71            config,
72            cache,
73            registry,
74        })
75    }
76
77    pub fn doctor(&self) -> DiagnosticReport {
78        let mut checks = Vec::new();
79
80        if std::env::var("WAYLAND_DISPLAY").is_ok() {
81            checks.push(DiagnosticCheck {
82                name: "Wayland Session".to_string(),
83                status: DiagnosticStatus::Pass,
84                message: "Wayland display socket detected.".to_string(),
85            });
86            checks.push(DiagnosticCheck {
87                    name: "wlr-layer-shell".to_string(),
88                    status: DiagnosticStatus::Warn,
89                    message: "Protocol probing occurs when the daemon binds layer-shell; run wallr daemon for a definitive check.".to_string(),
90                });
91        } else {
92            checks.push(DiagnosticCheck {
93                name: "Wayland Session".to_string(),
94                status: DiagnosticStatus::Fail,
95                message: "$WAYLAND_DISPLAY is not set. wallr requires a Wayland environment."
96                    .to_string(),
97            });
98        }
99
100        let provider_binary = match self.config.theme.provider {
101            ThemeProvider::Matugen => Some("matugen"),
102            ThemeProvider::Wallust => Some("wallust"),
103            ThemeProvider::Pywal => Some("wal"),
104            ThemeProvider::None => None,
105        };
106
107        if let Some(bin) = provider_binary {
108            let is_available = theme::check_provider_available(&self.config.theme.provider);
109            if is_available {
110                checks.push(DiagnosticCheck {
111                    name: format!("Theme Provider ({})", bin),
112                    status: DiagnosticStatus::Pass,
113                    message: format!("Binary '{}' is available on PATH.", bin),
114                });
115            } else {
116                checks.push(DiagnosticCheck {
117                    name: format!("Theme Provider ({})", bin),
118                    status: DiagnosticStatus::Fail,
119                    message: format!("Binary '{}' was not found on PATH.", bin),
120                });
121            }
122        }
123
124        if let Some(warning) = theme::detect_matugen_loop_risk() {
125            checks.push(DiagnosticCheck {
126                name: "Infinite Loop Risk".to_string(),
127                status: DiagnosticStatus::Warn,
128                message: warning,
129            });
130        } else {
131            checks.push(DiagnosticCheck {
132                name: "Infinite Loop Risk".to_string(),
133                status: DiagnosticStatus::Pass,
134                message: "No immediate parent loop risks detected.".to_string(),
135            });
136        }
137
138        DiagnosticReport { checks }
139    }
140
141    pub fn validate_animation(&self, path: &Path) -> Result<ValidationReport, WallpaperError> {
142        info!("Validating animation package: {:?}", path);
143        let mut checks = Vec::new();
144
145        match crate::packages::load_local_animation(path) {
146            Ok(spec) => {
147                checks.push(ValidationCheck {
148                    name: "YAML Syntax & Parsing".to_string(),
149                    passed: true,
150                    message: Some(format!("Successfully parsed spec '{}'", spec.name)),
151                });
152
153                match crate::animation::validate_animation(&spec) {
154                    Ok(_) => {
155                        checks.push(ValidationCheck {
156                            name: "Timeline & Effects Validation".to_string(),
157                            passed: true,
158                            message: Some(
159                                "Valid animation settings and timeline structure.".to_string(),
160                            ),
161                        });
162                    }
163                    Err(errs) => {
164                        let msg = errs
165                            .into_iter()
166                            .map(|e| e.to_string())
167                            .collect::<Vec<_>>()
168                            .join(", ");
169                        checks.push(ValidationCheck {
170                            name: "Timeline & Effects Validation".to_string(),
171                            passed: false,
172                            message: Some(msg),
173                        });
174                    }
175                }
176            }
177            Err(e) => {
178                checks.push(ValidationCheck {
179                    name: "YAML Syntax & Parsing".to_string(),
180                    passed: false,
181                    message: Some(e.to_string()),
182                });
183            }
184        }
185
186        Ok(ValidationReport { checks })
187    }
188
189    pub async fn set_wallpaper(
190        &mut self,
191        path: &Path,
192        options: &SetOptions,
193    ) -> Result<(), WallpaperError> {
194        info!("Setting wallpaper to: {:?}", path);
195
196        let result = (|| {
197            theme::run_hooks(&self.config.hooks.before)?;
198            self.apply_wallpaper(path)?;
199            if !options.no_theme {
200                let provider = options
201                    .theme_provider
202                    .as_ref()
203                    .unwrap_or(&self.config.theme.provider);
204                if *provider != ThemeProvider::None {
205                    theme::dispatch_theme(provider, path, &self.config.matugen)?;
206                }
207            }
208            theme::run_reload_list(&self.config.reload)?;
209            theme::run_hooks(&self.config.hooks.after)?;
210            Ok::<(), WallpaperError>(())
211        })();
212        if result.is_err() {
213            let _ = theme::run_hooks(&self.config.hooks.error);
214        }
215        result
216    }
217
218    fn apply_wallpaper(&self, path: &Path) -> Result<(), WallpaperError> {
219        if !path.is_file() {
220            return Err(WallpaperError::Custom(format!(
221                "wallpaper file not found: {}",
222                path.display()
223            )));
224        }
225        let _ = self.cache.cache_image(path);
226        info!(
227            "Applying wallpaper natively via Wayland layer-shell surface: {:?}",
228            path
229        );
230        Ok(())
231    }
232
233    pub fn reload(&self) -> Result<(), WallpaperError> {
234        info!("Running reload commands...");
235        theme::run_reload_list(&self.config.reload)?;
236        Ok(())
237    }
238}