leenfetch_core/modules/linux/desktop/
theme.rs1#![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 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 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 && 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 let gtk4_path = format!("{home}/.config/gtk-4.0/settings.ini");
65 if let Ok(content) = fs::read_to_string(>k4_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 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 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 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 (>k2, >k3) {
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"); 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}