Skip to main content

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