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