Skip to main content

leenfetch_core/modules/linux/desktop/
theme.rs

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