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 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}