1use crate::config::{MatugenConfig, ThemeProvider};
2use std::fs;
3use std::path::Path;
4use std::process::{Command, Stdio};
5
6#[derive(Debug, thiserror::Error)]
7pub enum ThemeError {
8 #[error("failed to spawn theme provider: {0}")]
9 SpawnError(#[from] std::io::Error),
10 #[error("theme provider exited with code {0}: {1}")]
11 NonZeroExit(i32, String),
12}
13
14pub fn dispatch_theme(
15 provider: &ThemeProvider,
16 image_path: &Path,
17 matugen_config: &MatugenConfig,
18) -> Result<(), ThemeError> {
19 match provider {
20 ThemeProvider::Matugen if !matugen_config.enabled => Ok(()),
21 ThemeProvider::Matugen => run_matugen(image_path, matugen_config),
22 ThemeProvider::Wallust => run_wallust(image_path),
23 ThemeProvider::Pywal => run_pywal(image_path),
24 ThemeProvider::None => Ok(()),
25 }
26}
27
28fn run_matugen(image_path: &Path, config: &MatugenConfig) -> Result<(), ThemeError> {
30 let mut cmd = Command::new("matugen");
31 cmd.arg("image")
32 .arg(image_path)
33 .arg("--mode")
34 .arg(&config.mode)
35 .arg("--type")
36 .arg(&config.scheme)
37 .arg("--contrast")
38 .arg(config.contrast.to_string())
39 .arg("--source-color-index")
40 .arg("0")
41 .stdout(Stdio::null())
42 .stderr(Stdio::null());
43
44 for arg in &config.args {
45 cmd.arg(arg);
46 }
47
48 let mut child = cmd.spawn().map_err(ThemeError::SpawnError)?;
49
50 if config.wait {
51 let status = child.wait().map_err(ThemeError::SpawnError)?;
52 if !status.success() {
53 return Err(ThemeError::NonZeroExit(
54 status.code().unwrap_or(-1),
55 "matugen failed".to_string(),
56 ));
57 }
58 }
59
60 Ok(())
61}
62
63fn run_wallust(image_path: &Path) -> Result<(), ThemeError> {
65 let mut cmd = Command::new("wallust");
66 cmd.arg("run")
67 .arg(image_path)
68 .stdout(Stdio::null())
69 .stderr(Stdio::null());
70
71 let status = cmd.status().map_err(ThemeError::SpawnError)?;
72 if !status.success() {
73 return Err(ThemeError::NonZeroExit(
74 status.code().unwrap_or(-1),
75 "wallust failed".to_string(),
76 ));
77 }
78
79 Ok(())
80}
81
82fn run_pywal(image_path: &Path) -> Result<(), ThemeError> {
84 let mut cmd = Command::new("wal");
85 cmd.arg("-i")
86 .arg(image_path)
87 .stdout(Stdio::null())
88 .stderr(Stdio::null());
89
90 let status = cmd.status().map_err(ThemeError::SpawnError)?;
91 if !status.success() {
92 return Err(ThemeError::NonZeroExit(
93 status.code().unwrap_or(-1),
94 "pywal failed".to_string(),
95 ));
96 }
97
98 Ok(())
99}
100
101pub fn check_provider_available(provider: &ThemeProvider) -> bool {
102 let binary = match provider {
103 ThemeProvider::Matugen => "matugen",
104 ThemeProvider::Wallust => "wallust",
105 ThemeProvider::Pywal => "wal",
106 ThemeProvider::None => return true,
107 };
108
109 Command::new("which")
110 .arg(binary)
111 .stdout(Stdio::null())
112 .stderr(Stdio::null())
113 .status()
114 .map(|out| out.success())
115 .unwrap_or(false)
116}
117
118pub fn detect_matugen_loop_risk() -> Option<String> {
120 if let Ok(status) = fs::read_to_string("/proc/self/status")
121 && let Some(ppid_line) = status.lines().find(|l| l.starts_with("PPid:"))
122 && let Some(ppid) = ppid_line.split_whitespace().nth(1)
123 {
124 let cmdline_path = format!("/proc/{}/cmdline", ppid);
125 if let Ok(cmdline) = fs::read_to_string(&cmdline_path)
126 && cmdline.contains("matugen")
127 {
128 return Some("Detected matugen as parent process. This might cause an infinite loop if wallr is triggered by matugen.".to_string());
129 }
130 }
131 None
132}
133
134pub fn run_hooks(hooks: &[String]) -> Result<(), ThemeError> {
136 for hook in hooks {
137 let status = Command::new("sh")
138 .arg("-c")
139 .arg(hook)
140 .status()
141 .map_err(ThemeError::SpawnError)?;
142
143 if !status.success() {
144 return Err(ThemeError::NonZeroExit(
145 status.code().unwrap_or(-1),
146 format!("hook failed: {}", hook),
147 ));
148 }
149 }
150 Ok(())
151}
152
153pub fn run_reload_list(commands: &[String]) -> Result<(), ThemeError> {
155 for cmd in commands {
156 if cmd.contains(' ') {
157 let _ = Command::new("sh")
158 .arg("-c")
159 .arg(cmd)
160 .stdout(Stdio::null())
161 .stderr(Stdio::null())
162 .status();
163 } else {
164 let _ = Command::new("pkill")
165 .arg("-SIGUSR2")
166 .arg(cmd)
167 .stdout(Stdio::null())
168 .stderr(Stdio::null())
169 .status();
170 }
171 }
172 Ok(())
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178
179 #[test]
180 fn test_check_provider_available_none() {
181 assert!(check_provider_available(&ThemeProvider::None));
182 }
183
184 #[test]
185 fn test_dispatch_none() {
186 let matugen_cfg = MatugenConfig {
187 enabled: false,
188 mode: "dark".to_string(),
189 scheme: "scheme-tonal-spot".to_string(),
190 contrast: 0,
191 wait: false,
192 args: vec![],
193 };
194 let res = dispatch_theme(&ThemeProvider::None, Path::new("test.jpg"), &matugen_cfg);
195 assert!(res.is_ok());
196 }
197
198 #[test]
199 fn test_run_hooks_empty() {
200 let res = run_hooks(&[]);
201 assert!(res.is_ok());
202 }
203
204 #[test]
205 fn test_detect_loop_risk() {
206 assert!(detect_matugen_loop_risk().is_none());
208 }
209}