Skip to main content

wallr_core/wallpaper/
mod.rs

1use crate::config::{ThemeProvider, WallrConfig};
2use crate::theme;
3use anyhow::Result;
4use std::path::Path;
5use tracing::info;
6
7#[derive(Debug, Clone)]
8pub struct SetOptions {
9    pub no_theme: bool,
10    /// Per-invocation theme provider override (wins over config.theme.provider).
11    pub theme_provider: Option<ThemeProvider>,
12    pub monitor: Option<String>,
13}
14
15#[derive(Debug, Clone)]
16pub enum DiagnosticStatus {
17    Pass,
18    Warn,
19    Fail,
20}
21
22#[derive(Debug, Clone)]
23pub struct DiagnosticCheck {
24    pub name: String,
25    pub status: DiagnosticStatus,
26    pub message: String,
27}
28
29#[derive(Debug, Clone)]
30pub struct DiagnosticReport {
31    pub checks: Vec<DiagnosticCheck>,
32}
33
34#[derive(Debug, thiserror::Error)]
35pub enum WallpaperError {
36    #[error("Theme error: {0}")]
37    Theme(#[from] crate::theme::ThemeError),
38    #[error("Custom error: {0}")]
39    Custom(String),
40}
41
42pub struct WallpaperEngine {
43    pub config: WallrConfig,
44}
45
46impl WallpaperEngine {
47    pub fn new(config: WallrConfig) -> Result<Self, WallpaperError> {
48        Ok(Self { config })
49    }
50
51    pub fn doctor(&self) -> DiagnosticReport {
52        let mut checks = Vec::new();
53
54        if std::env::var("WAYLAND_DISPLAY").is_ok() {
55            checks.push(DiagnosticCheck {
56                name: "Wayland Session".to_string(),
57                status: DiagnosticStatus::Pass,
58                message: "Wayland display socket detected.".to_string(),
59            });
60            checks.push(DiagnosticCheck {
61                    name: "wlr-layer-shell".to_string(),
62                    status: DiagnosticStatus::Warn,
63                    message: "Protocol probing occurs when the daemon binds layer-shell; run wallr daemon for a definitive check.".to_string(),
64                });
65        } else {
66            checks.push(DiagnosticCheck {
67                name: "Wayland Session".to_string(),
68                status: DiagnosticStatus::Fail,
69                message: "$WAYLAND_DISPLAY is not set. wallr requires a Wayland environment."
70                    .to_string(),
71            });
72        }
73
74        let provider_binary = match self.config.theme.provider {
75            ThemeProvider::Matugen => Some("matugen"),
76            ThemeProvider::Wallust => Some("wallust"),
77            ThemeProvider::Pywal => Some("wal"),
78            ThemeProvider::None => None,
79        };
80
81        if let Some(bin) = provider_binary {
82            let is_available = theme::check_provider_available(&self.config.theme.provider);
83            if is_available {
84                checks.push(DiagnosticCheck {
85                    name: format!("Theme Provider ({})", bin),
86                    status: DiagnosticStatus::Pass,
87                    message: format!("Binary '{}' is available on PATH.", bin),
88                });
89            } else {
90                checks.push(DiagnosticCheck {
91                    name: format!("Theme Provider ({})", bin),
92                    status: DiagnosticStatus::Fail,
93                    message: format!("Binary '{}' was not found on PATH.", bin),
94                });
95            }
96        }
97
98        if let Some(warning) = theme::detect_matugen_loop_risk() {
99            checks.push(DiagnosticCheck {
100                name: "Infinite Loop Risk".to_string(),
101                status: DiagnosticStatus::Warn,
102                message: warning,
103            });
104        } else {
105            checks.push(DiagnosticCheck {
106                name: "Infinite Loop Risk".to_string(),
107                status: DiagnosticStatus::Pass,
108                message: "No immediate parent loop risks detected.".to_string(),
109            });
110        }
111
112        DiagnosticReport { checks }
113    }
114
115    pub async fn set_wallpaper(
116        &mut self,
117        path: &Path,
118        options: &SetOptions,
119    ) -> Result<(), WallpaperError> {
120        self.set_wallpaper_blocking(path, options)
121    }
122
123    /// Synchronous implementation for callers that must run it on a blocking
124    /// worker. Theme providers and hooks spawn and wait for external
125    /// processes; running them directly on a Tokio worker stalls the IPC
126    /// loop and every other task on that runtime.
127    pub fn set_wallpaper_blocking(
128        &mut self,
129        path: &Path,
130        options: &SetOptions,
131    ) -> Result<(), WallpaperError> {
132        info!("Setting wallpaper to: {:?}", path);
133
134        let result = (|| {
135            theme::run_hooks(&self.config.hooks.before)?;
136            self.apply_wallpaper(path)?;
137            if !options.no_theme {
138                let provider = options
139                    .theme_provider
140                    .as_ref()
141                    .unwrap_or(&self.config.theme.provider);
142                if *provider != ThemeProvider::None {
143                    theme::dispatch_theme(provider, path, &self.config.matugen)?;
144                }
145            }
146            theme::run_reload_list(&self.config.reload)?;
147            theme::run_hooks(&self.config.hooks.after)?;
148            Ok::<(), WallpaperError>(())
149        })();
150        if result.is_err() {
151            let _ = theme::run_hooks(&self.config.hooks.error);
152        }
153        result
154    }
155
156    fn apply_wallpaper(&self, path: &Path) -> Result<(), WallpaperError> {
157        if !path.is_file() {
158            return Err(WallpaperError::Custom(format!(
159                "wallpaper file not found: {}",
160                path.display()
161            )));
162        }
163        info!(
164            "Applying wallpaper natively via Wayland layer-shell surface: {:?}",
165            path
166        );
167        Ok(())
168    }
169
170    pub fn reload(&self) -> Result<(), WallpaperError> {
171        self.reload_blocking()
172    }
173
174    /// Synchronous implementation for blocking workers; see
175    /// [`WallpaperEngine::set_wallpaper_blocking`].
176    pub fn reload_blocking(&self) -> Result<(), WallpaperError> {
177        info!("Running reload commands...");
178        theme::run_reload_list(&self.config.reload)?;
179        Ok(())
180    }
181}