leenfetch_core/modules/linux/desktop/
theme.rs

1use std::{env, fs, process::Command};
2
3pub fn get_theme(de: Option<&str>) -> Option<String> {
4    if env::var_os("DISPLAY").is_none() {
5        return None;
6    }
7
8    let de = de.unwrap_or("").to_lowercase();
9    let home = env::var("HOME").unwrap_or_default();
10
11    let mut gtk2 = None;
12    let mut gtk3 = None;
13    let mut gtk4 = None;
14    let mut kde = None;
15    let mut qt = None;
16
17    // KDE: look in kdeglobals
18    if de.contains("kde") || de.contains("plasma") {
19        let kde_paths = [
20            format!("{home}/.config/kdeglobals"),
21            "/etc/xdg/kdeglobals".to_string(),
22        ];
23
24        for path in kde_paths {
25            if let Ok(content) = fs::read_to_string(&path) {
26                if let Some(line) = content.lines().find(|l| l.starts_with("Name=")) {
27                    kde = Some(line.trim_start_matches("Name=").trim().to_string());
28                    break;
29                }
30            }
31        }
32    }
33
34    // GTK3: via gsettings
35    if let Ok(output) = Command::new("gsettings")
36        .args(["get", "org.gnome.desktop.interface", "gtk-theme"])
37        .output()
38    {
39        if output.status.success() {
40            let val = String::from_utf8_lossy(&output.stdout)
41                .trim()
42                .trim_matches('\'')
43                .to_string();
44            if !val.is_empty() {
45                gtk3 = Some(val.clone());
46                gtk2 = Some(val.clone()); // fallback
47            }
48        }
49    }
50
51    // GTK4: ~/.config/gtk-4.0/settings.ini → [Settings] gtk-theme-name
52    let gtk4_path = format!("{home}/.config/gtk-4.0/settings.ini");
53    if let Ok(content) = fs::read_to_string(&gtk4_path) {
54        for line in content.lines() {
55            if let Some(val) = line.trim().strip_prefix("gtk-theme-name=") {
56                gtk4 = Some(val.trim_matches('"').to_string());
57                break;
58            }
59        }
60    }
61
62    // GTK2: fallback to gtkrc
63    if gtk2.is_none() {
64        let gtk2_paths = [
65            format!("{home}/.gtkrc-2.0"),
66            "/etc/gtk-2.0/gtkrc".into(),
67            "/usr/share/gtk-2.0/gtkrc".into(),
68        ];
69        for path in gtk2_paths {
70            if let Ok(content) = fs::read_to_string(&path) {
71                for line in content.lines() {
72                    if let Some((_, val)) = line
73                        .trim_start()
74                        .strip_prefix("gtk-theme-name")
75                        .and_then(|l| l.split_once('='))
76                    {
77                        gtk2 = Some(val.trim().trim_matches('"').to_string());
78                        break;
79                    }
80                }
81            }
82            if gtk2.is_some() {
83                break;
84            }
85        }
86    }
87
88    // Qt: from qt5ct / qt6ct config files
89    let qt_paths = [
90        format!("{home}/.config/qt5ct/qt5ct.conf"),
91        format!("{home}/.config/qt6ct/qt6ct.conf"),
92    ];
93    for path in qt_paths {
94        if let Ok(content) = fs::read_to_string(&path) {
95            for line in content.lines() {
96                if let Some(val) = line.trim().strip_prefix("style=") {
97                    qt = Some(val.trim().to_string());
98                    break;
99                }
100            }
101        }
102        if qt.is_some() {
103            break;
104        }
105    }
106
107    // Compose final output
108    let mut result = Vec::new();
109
110    if let Some(val) = kde {
111        result.push(format!("{val} [KDE]"));
112    }
113
114    if let Some(val) = qt {
115        result.push(format!("{val} [Qt]"));
116    }
117
118    match (&gtk2, &gtk3) {
119        (Some(g2), Some(g3)) if g2 == g3 => result.push(format!("{g3} [GTK2/3]")),
120        (Some(g2), Some(g3)) => {
121            result.push(format!("{g2} [GTK2]"));
122            result.push(format!("{g3} [GTK3]"));
123        }
124        (Some(g2), None) => result.push(format!("{g2} [GTK2]")),
125        (None, Some(g3)) => result.push(format!("{g3} [GTK3]")),
126        _ => {}
127    }
128
129    if let Some(val) = gtk4 {
130        result.push(format!("{val} [GTK4]"));
131    }
132
133    if result.is_empty() {
134        None
135    } else {
136        Some(result.join(", "))
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::test_utils::EnvLock;
144    use std::fs;
145    use std::time::{SystemTime, UNIX_EPOCH};
146
147    #[test]
148    fn returns_none_without_display() {
149        let env_lock = EnvLock::acquire(&["DISPLAY"]);
150        env_lock.remove_var("DISPLAY");
151        assert!(get_theme(None).is_none());
152        drop(env_lock);
153    }
154
155    #[test]
156    fn collects_theme_from_config_files() {
157        let env_lock = EnvLock::acquire(&["DISPLAY", "HOME", "PATH"]);
158        env_lock.set_var("DISPLAY", ":0");
159        env_lock.set_var("PATH", "/nonexistent"); // avoid spawning host tools
160
161        let unique = SystemTime::now()
162            .duration_since(UNIX_EPOCH)
163            .unwrap()
164            .as_nanos();
165        let temp_home = std::env::temp_dir().join(format!("leenfetch_theme_test_{unique}"));
166        fs::create_dir_all(temp_home.join(".config/gtk-4.0")).unwrap();
167        fs::create_dir_all(temp_home.join(".config/qt5ct")).unwrap();
168
169        fs::write(
170            temp_home.join(".config/kdeglobals"),
171            "Name=Catppuccin Plasma\n",
172        )
173        .unwrap();
174        fs::write(
175            temp_home.join(".config/gtk-4.0/settings.ini"),
176            "[Settings]\ngtk-theme-name=Nordic\n",
177        )
178        .unwrap();
179        fs::write(temp_home.join(".gtkrc-2.0"), "gtk-theme-name=\"Adwaita\"\n").unwrap();
180        fs::write(temp_home.join(".config/qt5ct/qt5ct.conf"), "style=Breeze\n").unwrap();
181
182        env_lock.set_var("HOME", temp_home.to_str().unwrap());
183
184        let result = get_theme(Some("plasma")).expect("expected theme output");
185        assert!(
186            result.contains("[KDE]"),
187            "Expected KDE theme marker in {result}"
188        );
189        assert!(
190            result.contains("[GTK2]") || result.contains("[GTK2/3]"),
191            "Expected GTK theme marker in {result}"
192        );
193        assert!(
194            result.contains("[GTK4]"),
195            "Expected GTK4 theme marker in {result}"
196        );
197        assert!(
198            result.contains("[Qt]"),
199            "Expected Qt theme marker in {result}"
200        );
201
202        fs::remove_dir_all(&temp_home).unwrap();
203        drop(env_lock);
204    }
205}