leenfetch_core/modules/linux/desktop/
theme.rs1use 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 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 let gtk3_path = format!("{}/.config/gtk-3.0/settings.ini", home);
36 if let Ok(content) = fs::read_to_string(>k3_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 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 let gtk4_path = format!("{home}/.config/gtk-4.0/settings.ini");
67 if let Ok(content) = fs::read_to_string(>k4_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 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 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 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 (>k2, >k3) {
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"); 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}