1#[allow(dead_code)]
7fn parse_ini_key(content: &str, key: &str) -> Option<String> {
8 for line in content.lines() {
9 let line = line.trim();
10 if line.starts_with('#') || line.starts_with(';') {
11 continue;
12 }
13 if let Some(pos) = line.find('=') {
14 let k = line[..pos].trim();
15 if k == key {
16 let v = line[pos + 1..].trim();
17 let v = if (v.starts_with('"') && v.ends_with('"'))
18 || (v.starts_with('\'') && v.ends_with('\''))
19 {
20 if v.len() >= 2 {
21 v[1..v.len() - 1].to_string()
22 } else {
23 v.to_string()
24 }
25 } else {
26 v.to_string()
27 };
28 if !v.is_empty() {
29 return Some(v);
30 }
31 }
32 }
33 }
34 None
35}
36
37#[cfg(target_os = "linux")]
38fn get_gtk_setting(key: &str) -> Option<String> {
39 let home = dirs::home_dir()?;
40 let paths = [
41 home.join(".config/gtk-4.0/settings.ini"),
42 home.join(".config/gtk-3.0/settings.ini"),
43 home.join(".config/gtk-2.0/settings.ini"),
44 home.join(".gtkrc-2.0"),
45 ];
46 for path in &paths {
47 if path.exists() {
48 if let Ok(contents) = std::fs::read_to_string(path) {
49 if let Some(val) = parse_ini_key(&contents, key) {
50 return Some(val);
51 }
52 }
53 }
54 }
55 None
56}
57
58#[cfg(target_os = "linux")]
59fn query_gsettings(schema: &str, key: &str) -> Option<String> {
60 let output = std::process::Command::new("gsettings")
61 .args(["get", schema, key])
62 .output()
63 .ok()?;
64 if output.status.success() {
65 let val = String::from_utf8_lossy(&output.stdout).trim().to_string();
66 let val = val.trim_matches('\'').trim_matches('"').to_string();
67 if !val.is_empty() && val != "''" && val != "\"\"" {
68 return Some(val);
69 }
70 }
71 None
72}
73
74#[cfg(target_os = "linux")]
75fn get_kde_setting(key: &str) -> Option<String> {
76 let home = dirs::home_dir()?;
77 let path = home.join(".config/kdeglobals");
78 if path.exists() {
79 if let Ok(contents) = std::fs::read_to_string(path) {
80 return parse_ini_key(&contents, key);
81 }
82 }
83 None
84}
85
86pub(crate) fn get_default_monospace_font() -> Option<String> {
87 #[cfg(target_os = "linux")]
88 {
89 if let Ok(output) = std::process::Command::new("fc-match")
90 .arg("monospace")
91 .output()
92 {
93 if output.status.success() {
94 let s = String::from_utf8_lossy(&output.stdout);
95 if let Some(start) = s.find('"') {
96 if let Some(end) = s[start + 1..].find('"') {
97 return Some(s[start + 1..start + 1 + end].to_string());
98 }
99 }
100 }
101 }
102 return None;
103 }
104
105 #[cfg(target_os = "macos")]
106 {
107 return Some("SF Mono".to_string());
108 }
109
110 #[cfg(target_os = "windows")]
111 {
112 return Some("Consolas".to_string());
113 }
114
115 #[allow(unreachable_code)]
116 None
117}
118
119#[cfg(target_os = "linux")]
120pub(crate) fn detect_ui_theme_and_fonts() -> (
121 Option<String>,
122 Option<String>,
123 Option<String>,
124 Option<String>,
125) {
126 let gtk_theme = get_gtk_setting("gtk-theme-name")
127 .or_else(|| query_gsettings("org.gnome.desktop.interface", "gtk-theme"));
128 let gtk_icons = get_gtk_setting("gtk-icon-theme-name")
129 .or_else(|| query_gsettings("org.gnome.desktop.interface", "icon-theme"));
130 let gtk_cursor = get_gtk_setting("gtk-cursor-theme-name")
131 .or_else(|| query_gsettings("org.gnome.desktop.interface", "cursor-theme"));
132 let gtk_font = get_gtk_setting("gtk-font-name")
133 .or_else(|| query_gsettings("org.gnome.desktop.interface", "font-name"));
134
135 let qt_theme = get_kde_setting("widgetStyle").or_else(|| get_kde_setting("ColorScheme"));
136 let qt_icons = get_kde_setting("iconTheme");
137 let qt_cursor = {
138 let home = dirs::home_dir();
139 home.and_then(|h| {
140 let path = h.join(".config/kcminputrc");
141 if path.exists() {
142 std::fs::read_to_string(path)
143 .ok()
144 .and_then(|contents| parse_ini_key(&contents, "theme"))
145 } else {
146 None
147 }
148 })
149 };
150 let qt_font = get_kde_setting("font").map(|f| {
151 let parts: Vec<&str> = f.split(',').collect();
152 if parts.len() >= 2 {
153 let name = parts[0].trim();
154 let size = parts[1].trim();
155 format!("{} ({}pt)", name, size)
156 } else {
157 f
158 }
159 });
160
161 let de = std::env::var("XDG_CURRENT_DESKTOP")
162 .or_else(|_| std::env::var("DESKTOP_SESSION"))
163 .unwrap_or_default()
164 .to_lowercase();
165 let is_kde = de.contains("kde") || de.contains("plasma");
166
167 let theme = if is_kde {
168 match (qt_theme, gtk_theme) {
169 (Some(qt), Some(gt)) => Some(format!("{} [Qt], {} [GTK]", qt, gt)),
170 (Some(qt), None) => Some(format!("{} [Qt]", qt)),
171 (None, Some(gt)) => Some(format!("{} [GTK]", gt)),
172 (None, None) => None,
173 }
174 } else {
175 match (gtk_theme, qt_theme) {
176 (Some(gt), Some(qt)) => Some(format!("{} [GTK], {} [Qt]", gt, qt)),
177 (Some(gt), None) => Some(format!("{} [GTK]", gt)),
178 (None, Some(qt)) => Some(format!("{} [Qt]", qt)),
179 (None, None) => None,
180 }
181 };
182
183 let icons = if is_kde {
184 match (qt_icons, gtk_icons) {
185 (Some(qi), Some(gi)) => Some(format!("{} [Qt], {} [GTK]", qi, gi)),
186 (Some(qi), None) => Some(format!("{} [Qt]", qi)),
187 (None, Some(gi)) => Some(format!("{} [GTK]", gi)),
188 (None, None) => None,
189 }
190 } else {
191 match (gtk_icons, qt_icons) {
192 (Some(gi), Some(qi)) => Some(format!("{} [GTK], {} [Qt]", gi, qi)),
193 (Some(gi), None) => Some(format!("{} [GTK]", gi)),
194 (None, Some(qi)) => Some(format!("{} [Qt]", qi)),
195 (None, None) => None,
196 }
197 };
198
199 let cursor = if is_kde {
200 match (qt_cursor, gtk_cursor) {
201 (Some(qc), Some(gc)) => Some(format!("{} [Qt], {} [GTK]", qc, gc)),
202 (Some(qc), None) => Some(format!("{} [Qt]", qc)),
203 (None, Some(gc)) => Some(format!("{} [GTK]", gc)),
204 (None, None) => None,
205 }
206 } else {
207 match (gtk_cursor, qt_cursor) {
208 (Some(gc), Some(qc)) => Some(format!("{} [GTK], {} [Qt]", gc, qc)),
209 (Some(gc), None) => Some(format!("{} [GTK]", gc)),
210 (None, Some(qc)) => Some(format!("{} [Qt]", qc)),
211 (None, None) => None,
212 }
213 };
214
215 let font = if is_kde {
216 match (qt_font, gtk_font) {
217 (Some(qf), Some(gf)) => Some(format!("{} [Qt], {} [GTK]", qf, gf)),
218 (Some(qf), None) => Some(format!("{} [Qt]", qf)),
219 (None, Some(gf)) => Some(format!("{} [GTK]", gf)),
220 (None, None) => None,
221 }
222 } else {
223 match (gtk_font, qt_font) {
224 (Some(gf), Some(qf)) => Some(format!("{} [GTK], {} [Qt]", gf, qf)),
225 (Some(gf), None) => Some(format!("{} [GTK]", gf)),
226 (None, Some(qf)) => Some(format!("{} [Qt]", qf)),
227 (None, None) => None,
228 }
229 };
230
231 (theme, icons, cursor, font)
232}
233
234#[cfg(target_os = "macos")]
235pub(crate) fn detect_ui_theme_and_fonts() -> (
236 Option<String>,
237 Option<String>,
238 Option<String>,
239 Option<String>,
240) {
241 let theme = match crate::macos_ffi::get_macos_appearance() {
242 Some(style) => Some(format!("Aqua ({})", style)),
243 None => Some("Aqua (Light)".to_string()),
244 };
245
246 (theme, None, None, Some("San Francisco".to_string()))
247}
248
249#[cfg(target_os = "windows")]
250pub(crate) fn detect_ui_theme_and_fonts() -> (
251 Option<String>,
252 Option<String>,
253 Option<String>,
254 Option<String>,
255) {
256 use crate::win_reg;
257 let theme = {
258 let apps_light = win_reg::get_reg_u32(
259 win_reg::HKEY_CURRENT_USER,
260 "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
261 "AppsUseLightTheme",
262 );
263
264 let apps_dark = apps_light.map(|val| val == 0);
265
266 match apps_dark {
267 Some(true) => Some("Dark".to_string()),
268 Some(false) => Some("Light".to_string()),
269 None => {
270 let output = std::process::Command::new("reg")
271 .args([
272 "query",
273 r"HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
274 "/v",
275 "AppsUseLightTheme",
276 ])
277 .output()
278 .ok();
279
280 let cmd_dark = output.and_then(|o| {
281 if o.status.success() {
282 let s = String::from_utf8_lossy(&o.stdout);
283 if s.contains("0x0") {
284 Some(true)
285 } else if s.contains("0x1") {
286 Some(false)
287 } else {
288 None
289 }
290 } else {
291 None
292 }
293 });
294
295 match cmd_dark {
296 Some(true) => Some("Dark".to_string()),
297 Some(false) => Some("Light".to_string()),
298 None => Some("Unknown".to_string()),
299 }
300 }
301 }
302 };
303
304 (theme, None, None, Some("Segoe UI".to_string()))
305}
306
307#[allow(dead_code)]
308pub(crate) fn decode_percent_encoded(s: &str) -> String {
309 let mut bytes = Vec::with_capacity(s.len());
310 let raw = s.as_bytes();
311 let mut i = 0;
312 while i < raw.len() {
313 if raw[i] == b'%' && i + 2 < raw.len() {
314 if let (Ok(h1), Ok(h2)) = (
315 std::str::from_utf8(&raw[i + 1..i + 2]).map(|h| u8::from_str_radix(h, 16)),
316 std::str::from_utf8(&raw[i + 2..i + 3]).map(|h| u8::from_str_radix(h, 16)),
317 ) {
318 if let (Ok(h1), Ok(h2)) = (h1, h2) {
319 bytes.push((h1 << 4) | h2);
320 i += 3;
321 continue;
322 }
323 }
324 }
325 bytes.push(raw[i]);
326 i += 1;
327 }
328 String::from_utf8_lossy(&bytes).to_string()
329}
330
331#[allow(dead_code)]
332pub(crate) fn clean_wallpaper_uri(uri: &str) -> Option<String> {
333 let trimmed = uri.trim().trim_matches('\'').trim_matches('"').trim();
334 if trimmed.is_empty() || trimmed == "''" || trimmed == "\"\"" {
335 return None;
336 }
337 let without_file = if let Some(rest) = trimmed.strip_prefix("file://") {
338 rest
339 } else {
340 trimmed
341 };
342 let decoded = decode_percent_encoded(without_file);
343 let cleaned = decoded.trim().to_string();
344 if cleaned.is_empty() {
345 None
346 } else {
347 Some(cleaned)
348 }
349}
350
351#[allow(dead_code)]
352pub(crate) fn parse_kwin_theme(content: &str) -> Option<String> {
353 let mut in_kdec = false;
354 let mut theme = None;
355 let mut library_name = None;
356
357 for line in content.lines() {
358 let line = line.trim();
359 if line.starts_with('[') && line.ends_with(']') {
360 let section = &line[1..line.len() - 1];
361 in_kdec = section == "org.kde.kdecoration2" || section.contains("kdecoration");
362 continue;
363 }
364 if in_kdec {
365 if let Some(pos) = line.find('=') {
366 let k = line[..pos].trim();
367 let v = line[pos + 1..].trim().trim_matches('"').trim_matches('\'');
368 if k == "theme" && !v.is_empty() {
369 theme = Some(v.to_string());
370 } else if k == "library" || k == "libraryName" {
371 library_name = Some(v.to_string());
372 }
373 }
374 }
375 }
376
377 let raw = theme.or(library_name)?;
378 let mut cleaned = raw.as_str();
379 for prefix in &[
380 "__aurorae__svg__",
381 "__aurorae__qml__",
382 "svg__",
383 "qml_",
384 "org.kde.",
385 ] {
386 if let Some(rest) = cleaned.strip_prefix(prefix) {
387 cleaned = rest;
388 }
389 }
390 if cleaned.is_empty() {
391 None
392 } else {
393 Some(cleaned.to_string())
394 }
395}
396
397#[allow(dead_code)]
398pub(crate) fn parse_xfwm4_theme(content: &str) -> Option<String> {
399 for line in content.lines() {
400 let line = line.trim();
401 if line.contains("name=\"theme\"") || line.contains("name=\"/general/theme\"") {
402 if let Some(start) = line.find("value=\"") {
403 let rest = &line[start + 7..];
404 if let Some(end) = rest.find('"') {
405 let val = &rest[..end];
406 if !val.is_empty() {
407 return Some(val.to_string());
408 }
409 }
410 }
411 }
412 }
413 None
414}
415
416#[allow(dead_code)]
417pub(crate) fn parse_openbox_theme(content: &str) -> Option<String> {
418 let mut in_theme = false;
419 for line in content.lines() {
420 let line = line.trim();
421 if line.starts_with("<theme>") {
422 in_theme = true;
423 }
424 if line.ends_with("</theme>") {
425 in_theme = false;
426 }
427 if in_theme || line.contains("<theme>") {
428 if let Some(start) = line.find("<name>") {
429 let rest = &line[start + 6..];
430 if let Some(end) = rest.find("</name>") {
431 let val = rest[..end].trim();
432 if !val.is_empty() {
433 return Some(val.to_string());
434 }
435 }
436 }
437 }
438 }
439 None
440}
441
442#[allow(dead_code)]
443pub(crate) fn parse_fluxbox_theme(content: &str) -> Option<String> {
444 for line in content.lines() {
445 let line = line.trim();
446 if let Some(rest) = line.strip_prefix("session.styleFile:") {
447 let path_str = rest.trim();
448 let path = std::path::Path::new(path_str);
449 if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
450 if !name.is_empty() {
451 return Some(name.to_string());
452 }
453 }
454 }
455 }
456 None
457}
458
459#[allow(dead_code)]
460pub(crate) fn parse_icewm_theme(content: &str) -> Option<String> {
461 for line in content.lines() {
462 let line = line.trim();
463 if let Some(rest) = line.strip_prefix("Theme=") {
464 let val = rest.trim().trim_matches('"').trim_matches('\'');
465 if let Some(idx) = val.find('/') {
466 let theme_name = &val[..idx];
467 if !theme_name.is_empty() {
468 return Some(theme_name.to_string());
469 }
470 } else if !val.is_empty() {
471 return Some(val.to_string());
472 }
473 }
474 }
475 None
476}
477
478#[allow(dead_code)]
479pub(crate) fn parse_plasma_wallpaper(content: &str) -> Option<String> {
480 for line in content.lines() {
481 let line = line.trim();
482 if line.starts_with("Image=") {
483 let val = line.trim_start_matches("Image=").trim();
484 if let Some(cleaned) = clean_wallpaper_uri(val) {
485 return Some(cleaned);
486 }
487 } else if line.starts_with("usersWallpapers=") {
488 let val = line.trim_start_matches("usersWallpapers=").trim();
489 if let Some(first) = val.split(',').next() {
490 if let Some(cleaned) = clean_wallpaper_uri(first) {
491 return Some(cleaned);
492 }
493 }
494 }
495 }
496 None
497}
498
499#[allow(dead_code)]
500pub(crate) fn parse_xfce_wallpaper(content: &str) -> Option<String> {
501 for line in content.lines() {
502 let line = line.trim();
503 if (line.contains("last-image") || line.contains("image-path")) && line.contains("value=") {
504 if let Some(start) = line.find("value=\"") {
505 let rest = &line[start + 7..];
506 if let Some(end) = rest.find('"') {
507 let val = &rest[..end];
508 if let Some(cleaned) = clean_wallpaper_uri(val) {
509 return Some(cleaned);
510 }
511 }
512 }
513 }
514 }
515 None
516}
517
518#[allow(dead_code)]
519pub(crate) fn parse_hyprpaper_wallpaper(content: &str) -> Option<String> {
520 for line in content.lines() {
521 let line = line.trim();
522 if line.starts_with('#') {
523 continue;
524 }
525 if line.starts_with("wallpaper") {
526 if let Some(pos) = line.find('=') {
527 let rest = line[pos + 1..].trim();
528 if let Some(idx) = rest.rfind(',') {
529 let path = rest[idx + 1..].trim();
530 if let Some(cleaned) = clean_wallpaper_uri(path) {
531 return Some(cleaned);
532 }
533 }
534 }
535 } else if line.starts_with("preload") {
536 if let Some(pos) = line.find('=') {
537 let path = line[pos + 1..].trim();
538 if let Some(cleaned) = clean_wallpaper_uri(path) {
539 return Some(cleaned);
540 }
541 }
542 }
543 }
544 None
545}
546
547#[allow(dead_code)]
548pub(crate) fn parse_sway_wallpaper(content: &str) -> Option<String> {
549 for line in content.lines() {
550 let line = line.trim();
551 if line.starts_with('#') {
552 continue;
553 }
554 if line.starts_with("output ") && line.contains(" bg ") {
555 let parts: Vec<&str> = line.split_whitespace().collect();
556 if let Some(bg_idx) = parts.iter().position(|&p| p == "bg") {
557 if bg_idx + 1 < parts.len() {
558 return clean_wallpaper_uri(parts[bg_idx + 1]);
559 }
560 }
561 }
562 }
563 None
564}
565
566#[allow(dead_code)]
567pub(crate) fn parse_feh_wallpaper(content: &str) -> Option<String> {
568 for line in content.lines() {
569 let line = line.trim();
570 if line.starts_with("feh ") {
571 if let Some(start) = line.find('\'') {
572 if let Some(end) = line[start + 1..].rfind('\'') {
573 return clean_wallpaper_uri(&line[start + 1..start + 1 + end]);
574 }
575 }
576 if let Some(start) = line.find('"') {
577 if let Some(end) = line[start + 1..].rfind('"') {
578 return clean_wallpaper_uri(&line[start + 1..start + 1 + end]);
579 }
580 }
581 let parts: Vec<&str> = line.split_whitespace().collect();
582 if let Some(last) = parts.last() {
583 if !last.starts_with('-') {
584 return clean_wallpaper_uri(last);
585 }
586 }
587 }
588 }
589 None
590}
591
592#[allow(dead_code)]
593pub(crate) fn parse_nitrogen_wallpaper(content: &str) -> Option<String> {
594 for line in content.lines() {
595 let line = line.trim();
596 if line.starts_with("file=") {
597 return clean_wallpaper_uri(line.trim_start_matches("file="));
598 }
599 }
600 None
601}
602
603#[allow(dead_code)]
604pub(crate) fn parse_windows_theme_name(theme_path: &str) -> Option<String> {
605 let clean = theme_path.trim().trim_matches('"').trim_matches('\'');
606 let last_segment = clean.rsplit(['/', '\\']).next()?;
607 let stem = if let Some(idx) = last_segment.rfind('.') {
608 &last_segment[..idx]
609 } else {
610 last_segment
611 };
612 if stem.is_empty() {
613 return None;
614 }
615 match stem.to_lowercase().as_str() {
616 "aero" => Some("Aero".to_string()),
617 "dark" => Some("Dark".to_string()),
618 "light" => Some("Light".to_string()),
619 "custom" => Some("Custom".to_string()),
620 "windows" => Some("Windows".to_string()),
621 _ => Some(stem.to_string()),
622 }
623}
624
625pub(crate) fn detect_wm_theme(wm: Option<&str>, desktop: Option<&str>) -> Option<String> {
626 #[cfg(target_os = "linux")]
627 {
628 let wm_name = wm.map(|w| w.to_lowercase()).unwrap_or_default();
629 let de_name = desktop.map(|d| d.to_lowercase()).unwrap_or_default();
630 let home = dirs::home_dir();
631
632 if wm_name.contains("kwin") || de_name.contains("kde") || de_name.contains("plasma") {
634 if let Some(ref h) = home {
635 let kwinrc = h.join(".config/kwinrc");
636 if let Ok(content) = std::fs::read_to_string(&kwinrc) {
637 if let Some(theme) = parse_kwin_theme(&content) {
638 return Some(theme);
639 }
640 }
641 }
642 if let Some(kde_theme) =
643 get_kde_setting("widgetStyle").or_else(|| get_kde_setting("ColorScheme"))
644 {
645 return Some(kde_theme);
646 }
647 }
648
649 if wm_name.contains("mutter")
651 || de_name.contains("gnome")
652 || de_name.contains("budgie")
653 || de_name.contains("cinnamon")
654 {
655 if let Some(user_theme) =
656 query_gsettings("org.gnome.shell.extensions.user-theme", "name")
657 {
658 return Some(user_theme);
659 }
660 if let Some(wm_pref) = query_gsettings("org.gnome.desktop.wm.preferences", "theme") {
661 if !wm_pref.is_empty() && wm_pref != "Adwaita" {
662 return Some(wm_pref);
663 }
664 }
665 if let Some(cinna_theme) =
666 query_gsettings("org.cinnamon.desktop.wm.preferences", "theme")
667 {
668 return Some(cinna_theme);
669 }
670 if let Some(gtk) = get_gtk_setting("gtk-theme-name")
671 .or_else(|| query_gsettings("org.gnome.desktop.interface", "gtk-theme"))
672 {
673 return Some(gtk);
674 }
675 }
676
677 if wm_name.contains("marco") || de_name.contains("mate") {
679 if let Some(theme) = query_gsettings("org.mate.Marco.general", "theme") {
680 return Some(theme);
681 }
682 }
683
684 if wm_name.contains("xfwm") || de_name.contains("xfce") {
686 if let Some(ref h) = home {
687 let xfwm4_xml = h.join(".config/xfce4/xfconf/xfce-perchannel-xml/xfwm4.xml");
688 if let Ok(content) = std::fs::read_to_string(&xfwm4_xml) {
689 if let Some(theme) = parse_xfwm4_theme(&content) {
690 return Some(theme);
691 }
692 }
693 }
694 }
695
696 if wm_name.contains("openbox") {
698 if let Some(ref h) = home {
699 for path in &[
700 h.join(".config/openbox/rc.xml"),
701 h.join(".config/openbox/lxde-rc.xml"),
702 h.join(".config/openbox/lxqt-rc.xml"),
703 ] {
704 if let Ok(content) = std::fs::read_to_string(path) {
705 if let Some(theme) = parse_openbox_theme(&content) {
706 return Some(theme);
707 }
708 }
709 }
710 }
711 }
712
713 if wm_name.contains("fluxbox") {
715 if let Some(ref h) = home {
716 let init = h.join(".fluxbox/init");
717 if let Ok(content) = std::fs::read_to_string(&init) {
718 if let Some(theme) = parse_fluxbox_theme(&content) {
719 return Some(theme);
720 }
721 }
722 }
723 }
724
725 if wm_name.contains("icewm") {
727 if let Some(ref h) = home {
728 for path in &[h.join(".config/icewm/theme"), h.join(".icewm/theme")] {
729 if let Ok(content) = std::fs::read_to_string(path) {
730 if let Some(theme) = parse_icewm_theme(&content) {
731 return Some(theme);
732 }
733 }
734 }
735 }
736 }
737
738 if let Some(gtk) = get_gtk_setting("gtk-theme-name")
740 .or_else(|| query_gsettings("org.gnome.desktop.interface", "gtk-theme"))
741 {
742 return Some(gtk);
743 }
744 if let Some(qt) = get_kde_setting("widgetStyle").or_else(|| get_kde_setting("ColorScheme"))
745 {
746 return Some(qt);
747 }
748
749 return None;
750 }
751
752 #[cfg(target_os = "macos")]
753 {
754 let _ = (wm, desktop);
755 return match crate::macos_ffi::get_macos_appearance() {
756 Some(style) => Some(format!("Aqua ({})", style)),
757 None => Some("Aqua (Light)".to_string()),
758 };
759 }
760
761 #[cfg(target_os = "windows")]
762 {
763 let _ = (wm, desktop);
764 use crate::win_reg;
765 let theme_path = win_reg::get_reg_string(
766 win_reg::HKEY_CURRENT_USER,
767 "Software\\Microsoft\\Windows\\CurrentVersion\\Themes",
768 "CurrentTheme",
769 );
770 let apps_light = win_reg::get_reg_u32(
771 win_reg::HKEY_CURRENT_USER,
772 "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
773 "AppsUseLightTheme",
774 );
775 let dark_suffix = match apps_light {
776 Some(0) => " (Dark)",
777 Some(_) => " (Light)",
778 None => "",
779 };
780
781 if let Some(path) = theme_path {
782 if let Some(name) = parse_windows_theme_name(&path) {
783 return Some(format!("{}{}", name, dark_suffix));
784 }
785 }
786 return Some(format!("Aero{}", dark_suffix));
787 }
788
789 #[allow(unreachable_code)]
790 None
791}
792
793pub(crate) fn detect_wallpaper(desktop: Option<&str>, wm: Option<&str>) -> Option<String> {
794 #[cfg(target_os = "linux")]
795 {
796 let de_name = desktop.map(|d| d.to_lowercase()).unwrap_or_default();
797 let wm_name = wm.map(|w| w.to_lowercase()).unwrap_or_default();
798 let home = dirs::home_dir();
799
800 if de_name.contains("gnome")
802 || de_name.contains("budgie")
803 || de_name.contains("unity")
804 || wm_name.contains("mutter")
805 || wm_name.contains("niri")
806 {
807 if let Some(uri) = query_gsettings("org.gnome.desktop.background", "picture-uri-dark")
808 .or_else(|| query_gsettings("org.gnome.desktop.background", "picture-uri"))
809 {
810 if let Some(cleaned) = clean_wallpaper_uri(&uri) {
811 if !cleaned.is_empty() {
812 return Some(cleaned);
813 }
814 }
815 }
816 }
817 if de_name.contains("cinnamon") {
818 if let Some(uri) = query_gsettings("org.cinnamon.desktop.background", "picture-uri") {
819 if let Some(cleaned) = clean_wallpaper_uri(&uri) {
820 if !cleaned.is_empty() {
821 return Some(cleaned);
822 }
823 }
824 }
825 }
826
827 if de_name.contains("mate") || wm_name.contains("marco") {
829 if let Some(file) = query_gsettings("org.mate.background", "picture-filename") {
830 if let Some(cleaned) = clean_wallpaper_uri(&file) {
831 if !cleaned.is_empty() {
832 return Some(cleaned);
833 }
834 }
835 }
836 }
837
838 if de_name.contains("kde") || de_name.contains("plasma") || wm_name.contains("kwin") {
840 if let Some(ref h) = home {
841 let appletsrc = h.join(".config/plasma-org.kde.plasma.desktop-appletsrc");
842 if let Ok(content) = std::fs::read_to_string(&appletsrc) {
843 if let Some(wp) = parse_plasma_wallpaper(&content) {
844 return Some(wp);
845 }
846 }
847 }
848 }
849
850 if de_name.contains("xfce") || wm_name.contains("xfwm") {
852 if let Some(ref h) = home {
853 let xfce_xml = h.join(".config/xfce4/xfconf/xfce-perchannel-xml/xfce4-desktop.xml");
854 if let Ok(content) = std::fs::read_to_string(&xfce_xml) {
855 if let Some(wp) = parse_xfce_wallpaper(&content) {
856 return Some(wp);
857 }
858 }
859 }
860 }
861
862 if let Some(ref h) = home {
864 let conf = h.join(".config/hypr/hyprpaper.conf");
865 if let Ok(content) = std::fs::read_to_string(&conf) {
866 if let Some(wp) = parse_hyprpaper_wallpaper(&content) {
867 return Some(wp);
868 }
869 }
870 }
871
872 if let Some(ref h) = home {
874 for conf_path in &[h.join(".config/sway/config"), h.join(".sway/config")] {
875 if let Ok(content) = std::fs::read_to_string(conf_path) {
876 if let Some(wp) = parse_sway_wallpaper(&content) {
877 return Some(wp);
878 }
879 }
880 }
881 }
882
883 if let Some(ref h) = home {
885 let fehbg = h.join(".fehbg");
886 if let Ok(content) = std::fs::read_to_string(&fehbg) {
887 if let Some(wp) = parse_feh_wallpaper(&content) {
888 return Some(wp);
889 }
890 }
891 }
892
893 if let Some(ref h) = home {
895 let n_cfg = h.join(".config/nitrogen/bg-saved.cfg");
896 if let Ok(content) = std::fs::read_to_string(&n_cfg) {
897 if let Some(wp) = parse_nitrogen_wallpaper(&content) {
898 return Some(wp);
899 }
900 }
901 }
902
903 if let Some(uri) = query_gsettings("org.gnome.desktop.background", "picture-uri-dark")
905 .or_else(|| query_gsettings("org.gnome.desktop.background", "picture-uri"))
906 {
907 if let Some(cleaned) = clean_wallpaper_uri(&uri) {
908 if !cleaned.is_empty() {
909 return Some(cleaned);
910 }
911 }
912 }
913
914 return None;
915 }
916
917 #[cfg(target_os = "macos")]
918 {
919 let _ = (desktop, wm);
920 return crate::macos_ffi::get_macos_wallpaper();
921 }
922
923 #[cfg(target_os = "windows")]
924 {
925 let _ = (desktop, wm);
926 use crate::win_reg;
927 let wp = win_reg::get_reg_string(
928 win_reg::HKEY_CURRENT_USER,
929 "Control Panel\\Desktop",
930 "WallPaper",
931 );
932 return wp.and_then(|w| clean_wallpaper_uri(&w));
933 }
934
935 #[allow(unreachable_code)]
936 None
937}
938
939#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
940pub(crate) fn detect_ui_theme_and_fonts() -> (
941 Option<String>,
942 Option<String>,
943 Option<String>,
944 Option<String>,
945) {
946 (None, None, None, None)
947}
948
949#[cfg(test)]
950mod tests {
951 use super::*;
952
953 #[test]
954 fn test_parse_ini_key() {
955 let ini = "[Settings]\ngtk-theme-name=Adwaita-dark\ngtk-icon-theme-name=Papirus\n";
956 assert_eq!(
957 parse_ini_key(ini, "gtk-theme-name"),
958 Some("Adwaita-dark".to_string())
959 );
960 assert_eq!(
961 parse_ini_key(ini, "gtk-icon-theme-name"),
962 Some("Papirus".to_string())
963 );
964 assert_eq!(parse_ini_key(ini, "missing-key"), None);
965
966 let ini_quoted = "font=\"DejaVu Sans 11\"\ncursor='Adwaita'\n";
968 assert_eq!(
969 parse_ini_key(ini_quoted, "font"),
970 Some("DejaVu Sans 11".to_string())
971 );
972 assert_eq!(
973 parse_ini_key(ini_quoted, "cursor"),
974 Some("Adwaita".to_string())
975 );
976
977 let ini_comments = "# this is a comment\n; also a comment\nkey=value\n";
979 assert_eq!(
980 parse_ini_key(ini_comments, "key"),
981 Some("value".to_string())
982 );
983 assert_eq!(parse_ini_key(ini_comments, "#"), None);
984 }
985
986 #[test]
987 fn test_decode_percent_encoded() {
988 assert_eq!(decode_percent_encoded("hello%20world"), "hello world");
989 assert_eq!(
990 decode_percent_encoded("/usr/share/wallpapers/My%20Wallpaper.png"),
991 "/usr/share/wallpapers/My Wallpaper.png"
992 );
993 assert_eq!(decode_percent_encoded("no_percent"), "no_percent");
994 }
995
996 #[test]
997 fn test_clean_wallpaper_uri() {
998 assert_eq!(
999 clean_wallpaper_uri("'file:///usr/share/backgrounds/day.jpg'"),
1000 Some("/usr/share/backgrounds/day.jpg".to_string())
1001 );
1002 assert_eq!(
1003 clean_wallpaper_uri("\"file:///home/user/Pictures/My%20Wallpaper.png\""),
1004 Some("/home/user/Pictures/My Wallpaper.png".to_string())
1005 );
1006 assert_eq!(
1007 clean_wallpaper_uri("C:\\Users\\user\\Pictures\\Wallpaper.jpg"),
1008 Some("C:\\Users\\user\\Pictures\\Wallpaper.jpg".to_string())
1009 );
1010 assert_eq!(clean_wallpaper_uri(""), None);
1011 assert_eq!(clean_wallpaper_uri("''"), None);
1012 assert_eq!(clean_wallpaper_uri("\"\""), None);
1013 }
1014
1015 #[test]
1016 fn test_parse_kwin_theme() {
1017 let kwinrc = r#"
1018[org.kde.kdecoration2]
1019BorderSize=Normal
1020BorderSizeAuto=true
1021ButtonsOnLeft=M
1022ButtonsOnRight=IAX
1023library=org.kde.kwin.aurorae
1024theme=__aurorae__svg__Nordic
1025"#;
1026 assert_eq!(parse_kwin_theme(kwinrc), Some("Nordic".to_string()));
1027
1028 let kwinrc_breeze = r#"
1029[org.kde.kdecoration2]
1030library=org.kde.breeze
1031theme=Breeze
1032"#;
1033 assert_eq!(parse_kwin_theme(kwinrc_breeze), Some("Breeze".to_string()));
1034
1035 let kwinrc_qml = r#"
1036[org.kde.kdecoration2]
1037theme=qml_Sweet-Dark
1038"#;
1039 assert_eq!(parse_kwin_theme(kwinrc_qml), Some("Sweet-Dark".to_string()));
1040 }
1041
1042 #[test]
1043 fn test_parse_xfwm4_theme() {
1044 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1045<channel name="xfwm4" version="1.0">
1046 <property name="general" type="empty">
1047 <property name="theme" type="string" value="Greybird"/>
1048 <property name="title_font" type="string" value="Sans 9"/>
1049 </property>
1050</channel>
1051"#;
1052 assert_eq!(parse_xfwm4_theme(xml), Some("Greybird".to_string()));
1053 }
1054
1055 #[test]
1056 fn test_parse_openbox_theme() {
1057 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1058<openbox_config xmlns="http://openbox.org/3.4/rc">
1059 <theme>
1060 <name>Clearlooks</name>
1061 <titleLayout>NLIMC</titleLayout>
1062 </theme>
1063</openbox_config>
1064"#;
1065 assert_eq!(parse_openbox_theme(xml), Some("Clearlooks".to_string()));
1066 }
1067
1068 #[test]
1069 fn test_parse_fluxbox_theme() {
1070 let init = "session.styleFile: /usr/share/fluxbox/styles/bloody\nsession.screen0.toolbar.tools: prevworkspace, workspacename\n";
1071 assert_eq!(parse_fluxbox_theme(init), Some("bloody".to_string()));
1072 }
1073
1074 #[test]
1075 fn test_parse_icewm_theme() {
1076 let cfg = "Theme=\"Adwaita/default.theme\"\n";
1077 assert_eq!(parse_icewm_theme(cfg), Some("Adwaita".to_string()));
1078 }
1079
1080 #[test]
1081 fn test_parse_plasma_wallpaper() {
1082 let appletsrc = r#"
1083[Containments][1][Applets][2][Configuration][General]
1084Image=file:///usr/share/wallpapers/Next/contents/images/3840x2160.png
1085usersWallpapers=file:///usr/share/wallpapers/Next/contents/images/3840x2160.png
1086"#;
1087 assert_eq!(
1088 parse_plasma_wallpaper(appletsrc),
1089 Some("/usr/share/wallpapers/Next/contents/images/3840x2160.png".to_string())
1090 );
1091 }
1092
1093 #[test]
1094 fn test_parse_xfce_wallpaper() {
1095 let xml = r#"<?xml version="1.0" encoding="UTF-8"?>
1096<channel name="xfce4-desktop" version="1.0">
1097 <property name="backdrop" type="empty">
1098 <property name="screen0" type="empty">
1099 <property name="monitor0" type="empty">
1100 <property name="last-image" type="string" value="/usr/share/backgrounds/xfce/xfce-teal.jpg"/>
1101 </property>
1102 </property>
1103 </property>
1104</channel>
1105"#;
1106 assert_eq!(
1107 parse_xfce_wallpaper(xml),
1108 Some("/usr/share/backgrounds/xfce/xfce-teal.jpg".to_string())
1109 );
1110 }
1111
1112 #[test]
1113 fn test_parse_hyprpaper_wallpaper() {
1114 let conf = r#"
1115preload = /home/user/Pictures/wall.png
1116wallpaper = DP-1,/home/user/Pictures/wall.png
1117"#;
1118 assert_eq!(
1119 parse_hyprpaper_wallpaper(conf),
1120 Some("/home/user/Pictures/wall.png".to_string())
1121 );
1122 }
1123
1124 #[test]
1125 fn test_parse_sway_wallpaper() {
1126 let conf = r#"
1127output * bg /usr/share/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png fill
1128"#;
1129 assert_eq!(
1130 parse_sway_wallpaper(conf),
1131 Some("/usr/share/backgrounds/sway/Sway_Wallpaper_Blue_1920x1080.png".to_string())
1132 );
1133 }
1134
1135 #[test]
1136 fn test_parse_feh_wallpaper() {
1137 let feh = "feh --no-fehbg --bg-fill '/home/user/Pictures/nature.jpg'";
1138 assert_eq!(
1139 parse_feh_wallpaper(feh),
1140 Some("/home/user/Pictures/nature.jpg".to_string())
1141 );
1142 }
1143
1144 #[test]
1145 fn test_parse_nitrogen_wallpaper() {
1146 let cfg = "[xin_0]\nfile=/home/user/wallpapers/sunset.png\nmode=4\n";
1147 assert_eq!(
1148 parse_nitrogen_wallpaper(cfg),
1149 Some("/home/user/wallpapers/sunset.png".to_string())
1150 );
1151 }
1152
1153 #[test]
1154 fn test_parse_windows_theme_name() {
1155 assert_eq!(
1156 parse_windows_theme_name(r"C:\Windows\resources\Themes\aero.theme"),
1157 Some("Aero".to_string())
1158 );
1159 assert_eq!(
1160 parse_windows_theme_name(r"C:\Windows\resources\Themes\dark.theme"),
1161 Some("Dark".to_string())
1162 );
1163 assert_eq!(
1164 parse_windows_theme_name(
1165 r"C:\Users\User\AppData\Local\Microsoft\Windows\Themes\Custom.theme"
1166 ),
1167 Some("Custom".to_string())
1168 );
1169 }
1170}