1#[cfg(target_os = "linux")]
7fn parse_ini_key(content: &str, key: &str) -> Option<String> {
8 for line in content.lines() {
9 let line = line.trim();
10 if line.starts_with('#') || line.starts_with(';') {
11 continue;
12 }
13 if let Some(pos) = line.find('=') {
14 let k = line[..pos].trim();
15 if k == key {
16 let v = line[pos + 1..].trim();
17 let v = if (v.starts_with('"') && v.ends_with('"'))
18 || (v.starts_with('\'') && v.ends_with('\''))
19 {
20 if v.len() >= 2 {
21 v[1..v.len() - 1].to_string()
22 } else {
23 v.to_string()
24 }
25 } else {
26 v.to_string()
27 };
28 if !v.is_empty() {
29 return Some(v);
30 }
31 }
32 }
33 }
34 None
35}
36
37#[cfg(target_os = "linux")]
38fn get_gtk_setting(key: &str) -> Option<String> {
39 let home = dirs::home_dir()?;
40 let paths = [
41 home.join(".config/gtk-4.0/settings.ini"),
42 home.join(".config/gtk-3.0/settings.ini"),
43 home.join(".config/gtk-2.0/settings.ini"),
44 home.join(".gtkrc-2.0"),
45 ];
46 for path in &paths {
47 if path.exists() {
48 if let Ok(contents) = std::fs::read_to_string(path) {
49 if let Some(val) = parse_ini_key(&contents, key) {
50 return Some(val);
51 }
52 }
53 }
54 }
55 None
56}
57
58#[cfg(target_os = "linux")]
59fn query_gsettings(schema: &str, key: &str) -> Option<String> {
60 let output = std::process::Command::new("gsettings")
61 .args(["get", schema, key])
62 .output()
63 .ok()?;
64 if output.status.success() {
65 let val = String::from_utf8_lossy(&output.stdout).trim().to_string();
66 let val = val.trim_matches('\'').trim_matches('"').to_string();
67 if !val.is_empty() && val != "''" && val != "\"\"" {
68 return Some(val);
69 }
70 }
71 None
72}
73
74#[cfg(target_os = "linux")]
75fn get_kde_setting(key: &str) -> Option<String> {
76 let home = dirs::home_dir()?;
77 let path = home.join(".config/kdeglobals");
78 if path.exists() {
79 if let Ok(contents) = std::fs::read_to_string(path) {
80 return parse_ini_key(&contents, key);
81 }
82 }
83 None
84}
85
86pub(crate) fn get_default_monospace_font() -> Option<String> {
87 #[cfg(target_os = "linux")]
88 {
89 if let Ok(output) = std::process::Command::new("fc-match")
90 .arg("monospace")
91 .output()
92 {
93 if output.status.success() {
94 let s = String::from_utf8_lossy(&output.stdout);
95 if let Some(start) = s.find('"') {
96 if let Some(end) = s[start + 1..].find('"') {
97 return Some(s[start + 1..start + 1 + end].to_string());
98 }
99 }
100 }
101 }
102 return None;
103 }
104
105 #[cfg(target_os = "macos")]
106 {
107 return Some("SF Mono".to_string());
108 }
109
110 #[cfg(target_os = "windows")]
111 {
112 return Some("Consolas".to_string());
113 }
114
115 #[allow(unreachable_code)]
116 None
117}
118
119#[cfg(target_os = "linux")]
120pub(crate) fn detect_ui_theme_and_fonts() -> (
121 Option<String>,
122 Option<String>,
123 Option<String>,
124 Option<String>,
125) {
126 let gtk_theme = get_gtk_setting("gtk-theme-name")
127 .or_else(|| query_gsettings("org.gnome.desktop.interface", "gtk-theme"));
128 let gtk_icons = get_gtk_setting("gtk-icon-theme-name")
129 .or_else(|| query_gsettings("org.gnome.desktop.interface", "icon-theme"));
130 let gtk_cursor = get_gtk_setting("gtk-cursor-theme-name")
131 .or_else(|| query_gsettings("org.gnome.desktop.interface", "cursor-theme"));
132 let gtk_font = get_gtk_setting("gtk-font-name")
133 .or_else(|| query_gsettings("org.gnome.desktop.interface", "font-name"));
134
135 let qt_theme = get_kde_setting("widgetStyle").or_else(|| get_kde_setting("ColorScheme"));
136 let qt_icons = get_kde_setting("iconTheme");
137 let qt_cursor = {
138 let home = dirs::home_dir();
139 home.and_then(|h| {
140 let path = h.join(".config/kcminputrc");
141 if path.exists() {
142 std::fs::read_to_string(path)
143 .ok()
144 .and_then(|contents| parse_ini_key(&contents, "theme"))
145 } else {
146 None
147 }
148 })
149 };
150 let qt_font = get_kde_setting("font").map(|f| {
151 let parts: Vec<&str> = f.split(',').collect();
152 if parts.len() >= 2 {
153 let name = parts[0].trim();
154 let size = parts[1].trim();
155 format!("{} ({}pt)", name, size)
156 } else {
157 f
158 }
159 });
160
161 let de = std::env::var("XDG_CURRENT_DESKTOP")
162 .or_else(|_| std::env::var("DESKTOP_SESSION"))
163 .unwrap_or_default()
164 .to_lowercase();
165 let is_kde = de.contains("kde") || de.contains("plasma");
166
167 let theme = if is_kde {
168 match (qt_theme, gtk_theme) {
169 (Some(qt), Some(gt)) => Some(format!("{} [Qt], {} [GTK]", qt, gt)),
170 (Some(qt), None) => Some(format!("{} [Qt]", qt)),
171 (None, Some(gt)) => Some(format!("{} [GTK]", gt)),
172 (None, None) => None,
173 }
174 } else {
175 match (gtk_theme, qt_theme) {
176 (Some(gt), Some(qt)) => Some(format!("{} [GTK], {} [Qt]", gt, qt)),
177 (Some(gt), None) => Some(format!("{} [GTK]", gt)),
178 (None, Some(qt)) => Some(format!("{} [Qt]", qt)),
179 (None, None) => None,
180 }
181 };
182
183 let icons = if is_kde {
184 match (qt_icons, gtk_icons) {
185 (Some(qi), Some(gi)) => Some(format!("{} [Qt], {} [GTK]", qi, gi)),
186 (Some(qi), None) => Some(format!("{} [Qt]", qi)),
187 (None, Some(gi)) => Some(format!("{} [GTK]", gi)),
188 (None, None) => None,
189 }
190 } else {
191 match (gtk_icons, qt_icons) {
192 (Some(gi), Some(qi)) => Some(format!("{} [GTK], {} [Qt]", gi, qi)),
193 (Some(gi), None) => Some(format!("{} [GTK]", gi)),
194 (None, Some(qi)) => Some(format!("{} [Qt]", qi)),
195 (None, None) => None,
196 }
197 };
198
199 let cursor = if is_kde {
200 match (qt_cursor, gtk_cursor) {
201 (Some(qc), Some(gc)) => Some(format!("{} [Qt], {} [GTK]", qc, gc)),
202 (Some(qc), None) => Some(format!("{} [Qt]", qc)),
203 (None, Some(gc)) => Some(format!("{} [GTK]", gc)),
204 (None, None) => None,
205 }
206 } else {
207 match (gtk_cursor, qt_cursor) {
208 (Some(gc), Some(qc)) => Some(format!("{} [GTK], {} [Qt]", gc, qc)),
209 (Some(gc), None) => Some(format!("{} [GTK]", gc)),
210 (None, Some(qc)) => Some(format!("{} [Qt]", qc)),
211 (None, None) => None,
212 }
213 };
214
215 let font = if is_kde {
216 match (qt_font, gtk_font) {
217 (Some(qf), Some(gf)) => Some(format!("{} [Qt], {} [GTK]", qf, gf)),
218 (Some(qf), None) => Some(format!("{} [Qt]", qf)),
219 (None, Some(gf)) => Some(format!("{} [GTK]", gf)),
220 (None, None) => None,
221 }
222 } else {
223 match (gtk_font, qt_font) {
224 (Some(gf), Some(qf)) => Some(format!("{} [GTK], {} [Qt]", gf, qf)),
225 (Some(gf), None) => Some(format!("{} [GTK]", gf)),
226 (None, Some(qf)) => Some(format!("{} [Qt]", qf)),
227 (None, None) => None,
228 }
229 };
230
231 (theme, icons, cursor, font)
232}
233
234#[cfg(target_os = "macos")]
235pub(crate) fn detect_ui_theme_and_fonts() -> (
236 Option<String>,
237 Option<String>,
238 Option<String>,
239 Option<String>,
240) {
241 let theme = match crate::macos_ffi::get_macos_appearance() {
242 Some(style) => Some(format!("Aqua ({})", style)),
243 None => Some("Aqua (Light)".to_string()),
244 };
245
246 (theme, None, None, Some("San Francisco".to_string()))
247}
248
249#[cfg(target_os = "windows")]
250pub(crate) fn detect_ui_theme_and_fonts() -> (
251 Option<String>,
252 Option<String>,
253 Option<String>,
254 Option<String>,
255) {
256 use crate::win_reg;
257 let theme = {
258 let apps_light = win_reg::get_reg_u32(
259 win_reg::HKEY_CURRENT_USER,
260 "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
261 "AppsUseLightTheme",
262 );
263
264 let apps_dark = apps_light.map(|val| val == 0);
265
266 match apps_dark {
267 Some(true) => Some("Dark".to_string()),
268 Some(false) => Some("Light".to_string()),
269 None => {
270 let output = std::process::Command::new("reg")
271 .args([
272 "query",
273 r"HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
274 "/v",
275 "AppsUseLightTheme",
276 ])
277 .output()
278 .ok();
279
280 let cmd_dark = output.and_then(|o| {
281 if o.status.success() {
282 let s = String::from_utf8_lossy(&o.stdout);
283 if s.contains("0x0") {
284 Some(true)
285 } else if s.contains("0x1") {
286 Some(false)
287 } else {
288 None
289 }
290 } else {
291 None
292 }
293 });
294
295 match cmd_dark {
296 Some(true) => Some("Dark".to_string()),
297 Some(false) => Some("Light".to_string()),
298 None => Some("Unknown".to_string()),
299 }
300 }
301 }
302 };
303
304 (theme, None, None, Some("Segoe UI".to_string()))
305}
306
307#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
308pub(crate) fn detect_ui_theme_and_fonts() -> (
309 Option<String>,
310 Option<String>,
311 Option<String>,
312 Option<String>,
313) {
314 (None, None, None, None)
315}
316
317#[cfg(all(test, target_os = "linux"))]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_parse_ini_key() {
323 let ini = "[Settings]\ngtk-theme-name=Adwaita-dark\ngtk-icon-theme-name=Papirus\n";
324 assert_eq!(
325 parse_ini_key(ini, "gtk-theme-name"),
326 Some("Adwaita-dark".to_string())
327 );
328 assert_eq!(
329 parse_ini_key(ini, "gtk-icon-theme-name"),
330 Some("Papirus".to_string())
331 );
332 assert_eq!(parse_ini_key(ini, "missing-key"), None);
333
334 let ini_quoted = "font=\"DejaVu Sans 11\"\ncursor='Adwaita'\n";
336 assert_eq!(
337 parse_ini_key(ini_quoted, "font"),
338 Some("DejaVu Sans 11".to_string())
339 );
340 assert_eq!(
341 parse_ini_key(ini_quoted, "cursor"),
342 Some("Adwaita".to_string())
343 );
344
345 let ini_comments = "# this is a comment\n; also a comment\nkey=value\n";
347 assert_eq!(
348 parse_ini_key(ini_comments, "key"),
349 Some("value".to_string())
350 );
351 assert_eq!(parse_ini_key(ini_comments, "#"), None);
352 }
353}