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        info!("Setting wallpaper to: {:?}", path);
121
122        let result = (|| {
123            theme::run_hooks(&self.config.hooks.before)?;
124            self.apply_wallpaper(path)?;
125            if !options.no_theme {
126                let provider = options
127                    .theme_provider
128                    .as_ref()
129                    .unwrap_or(&self.config.theme.provider);
130                if *provider != ThemeProvider::None {
131                    theme::dispatch_theme(provider, path, &self.config.matugen)?;
132                }
133            }
134            theme::run_reload_list(&self.config.reload)?;
135            theme::run_hooks(&self.config.hooks.after)?;
136            Ok::<(), WallpaperError>(())
137        })();
138        if result.is_err() {
139            let _ = theme::run_hooks(&self.config.hooks.error);
140        }
141        result
142    }
143
144    fn apply_wallpaper(&self, path: &Path) -> Result<(), WallpaperError> {
145        if !path.is_file() {
146            return Err(WallpaperError::Custom(format!(
147                "wallpaper file not found: {}",
148                path.display()
149            )));
150        }
151        info!(
152            "Applying wallpaper natively via Wayland layer-shell surface: {:?}",
153            path
154        );
155        Ok(())
156    }
157
158    pub fn reload(&self) -> Result<(), WallpaperError> {
159        info!("Running reload commands...");
160        theme::run_reload_list(&self.config.reload)?;
161        Ok(())
162    }
163}