theme_detector/
qt.rs

1// SPDX-FileCopyrightText: 2023 CELESTIFYX Team
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::{
5    fs::File,
6    iter::Peekable,
7    str::Chars,
8
9    io::{
10        BufRead,
11        BufReader
12    }
13};
14
15use crate::{
16    constants::{
17        DE_PLASMA,
18        DE_LXQT
19    },
20
21    properties::{
22        parse_prop_line,
23        get_config_dirs,
24        parse_prop_file,
25        parse_prop_file_config,
26        parse_prop_file_config_values
27    }
28};
29
30#[derive(Default)]
31pub struct QtResult {
32    pub widget_style: String,
33    pub color_scheme: String,
34    pub icons:        String,
35    pub font:         String,
36    pub wallpaper:    String
37}
38
39impl QtResult {
40    fn all_set(&self) -> bool {
41        !self.widget_style.is_empty() && !self.color_scheme.is_empty() && !self.icons.is_empty() && !self.font.is_empty() && !self.wallpaper.is_empty()
42    }
43}
44
45#[derive(Debug, PartialEq)]
46enum PlasmaCategory {
47    General,
48    Kde,
49    Icons,
50    Other
51}
52
53fn detect_plasma_from_file(filename: &str, result: &mut QtResult) -> bool {
54    let file: File = match File::open(filename) {
55        Ok(f) => f,
56        Err(_) => return false
57    };
58
59    let reader: BufReader<File> = BufReader::new(file);
60    let mut category: PlasmaCategory = PlasmaCategory::Other;
61
62    for line in reader.lines().flatten() {
63        let line: &str = line.trim();
64
65        if line.starts_with('[') {
66            if let Some(end) = line.find(']') {
67                let cat_name: &str = &line[1..end];
68
69                category = match cat_name.to_lowercase().as_str() {
70                    "general" => PlasmaCategory::General,
71                    "kde"     => PlasmaCategory::Kde,
72                    "icons"   => PlasmaCategory::Icons,
73
74                    _         => PlasmaCategory::Other
75                };
76            }
77
78            continue;
79        }
80
81        match category {
82            PlasmaCategory::Kde if result.widget_style.is_empty() => {
83                if let Some(value) = parse_prop_line(line, "widgetStyle =") {
84                    result.widget_style = value;
85                }
86            },
87
88            PlasmaCategory::Icons if result.icons.is_empty() => {
89                if let Some(value) = parse_prop_line(line, "Theme =") {
90                    result.icons = value;
91                }
92            },
93
94            PlasmaCategory::General => {
95                if result.color_scheme.is_empty() && let Some(value) = parse_prop_line(line, "ColorScheme =") {
96                    result.color_scheme = value;
97                }
98
99                if result.font.is_empty() {
100                    if let Some(value) = parse_prop_line(line, "font =") {
101                        result.font = value;
102                    } else if let Some(value) = parse_prop_line(line, "Font =") {
103                        result.font = value;
104                    }
105                }
106            },
107
108            _ => {}
109        }
110    }
111
112    true
113}
114
115fn detect_plasma(result: &mut QtResult) -> () {
116    let config_dirs: Vec<String> = get_config_dirs();
117    let mut found_file: bool = false;
118
119    for config_dir in &config_dirs {
120        let kdeglobals: String = format!("{}kdeglobals", config_dir);
121
122        if detect_plasma_from_file(&kdeglobals, result) {
123            found_file = true;
124        }
125
126        let plasma_rc: String = format!("{}plasma-org.kde.plasma.desktop-appletsrc", config_dir);
127
128        if let Some(wp) = parse_prop_file(&plasma_rc, "Image=") && result.wallpaper.is_empty() {
129            result.wallpaper = wp;
130        }
131
132        if result.all_set() {
133            return;
134        }
135    }
136
137    if !found_file {
138        return;
139    }
140
141    if result.widget_style.is_empty() {
142        result.widget_style = "Breeze".to_string();
143    }
144
145    if result.color_scheme.is_empty() {
146        result.color_scheme = "BreezeLight".to_string();
147    }
148
149    if result.icons.is_empty() {
150        result.icons = "Breeze".to_string();
151    }
152
153    if result.font.is_empty() {
154        result.font = "Noto Sans, 10".to_string();
155    }
156}
157
158fn detect_lxqt(result: &mut QtResult) -> () {
159    let config_dirs: Vec<String> = get_config_dirs();
160
161    let mut queries: Vec<(&str, String)> = vec![
162        ("style = ",      String::new()),
163        ("icon_theme = ", String::new()),
164        ("font = ",       String::new())
165    ];
166
167    parse_prop_file_config_values(&config_dirs, "lxqt/lxqt.conf", &mut queries);
168
169    if result.widget_style.is_empty() && !queries[0].1.is_empty() {
170        result.widget_style = queries[0].1.clone();
171    }
172
173    if result.icons.is_empty() && !queries[1].1.is_empty() {
174        result.icons = queries[1].1.clone();
175    }
176
177    if result.font.is_empty() && !queries[2].1.is_empty() {
178        result.font = queries[2].1.clone();
179    }
180
181    if let Some(wp) = parse_prop_file_config(&config_dirs, "pcmanfm-qt/lxqt/settings.conf", "Wallpaper=") && result.wallpaper.is_empty() {
182        result.wallpaper = wp;
183    }
184}
185
186fn detect_qtct(qver: char, result: &mut QtResult) -> () {
187    let config_dirs: Vec<String> = get_config_dirs();
188    let config_file: String = format!("qt{}ct/qt{}ct.conf", qver, qver);
189
190    let mut queries: Vec<(&str, String)> = vec![
191        ("style=",      String::new()),
192        ("icon_theme=", String::new()),
193        ("font=",       String::new())
194    ];
195
196    parse_prop_file_config_values(&config_dirs, &config_file, &mut queries);
197
198    if result.widget_style.is_empty() && !queries[0].1.is_empty() {
199        result.widget_style = queries[0].1.clone();
200    }
201
202    if result.icons.is_empty() && !queries[1].1.is_empty() {
203        result.icons = queries[1].1.clone();
204    }
205
206    let font_raw: &String = &queries[2].1;
207
208    if !font_raw.is_empty() && result.font.is_empty() {
209        if font_raw.starts_with('@') {
210            result.font = format!("qt{}ct", qver);
211        } else if qver == '5' {
212            result.font = decode_qt5_string(font_raw);
213        } else {
214            result.font = font_raw.clone();
215        }
216    }
217}
218
219fn decode_qt5_string(input: &str) -> String {
220    let mut result: String = String::new();
221    let mut chars: Peekable<Chars> = input.chars().peekable();
222
223    while let Some(ch) = (&mut chars).next() {
224        if (ch == '\\') && (&mut chars).peek() == Some(&'x') {
225            (&mut chars).next();
226            let hex: String = (&mut chars).by_ref().take(4).collect();
227
228            if (hex.len() == 4) && let Ok(codepoint) = u32::from_str_radix(&hex, 16) && let Some(unicode_char) = char::from_u32(codepoint) {
229                (&mut result).push(unicode_char);
230                continue;
231            }
232
233            (&mut result).push('\\');
234            (&mut result).push('x');
235            (&mut result).push_str(&hex);
236        } else {
237            (&mut result).push(ch);
238        }
239    }
240
241    result
242}
243
244fn detect_kvantum(result: &mut QtResult) -> () {
245    let config_dirs: Vec<String> = get_config_dirs();
246
247    if let Some(theme) = parse_prop_file_config(&config_dirs, "Kvantum/kvantum.kvconfig", "theme=") && result.widget_style.is_empty() {
248        result.widget_style = theme;
249    }
250}
251
252pub fn detect_qt(de_name: Option<&str>) -> QtResult {
253    let mut result: QtResult = QtResult::default();
254
255    if let Some(de) = de_name {
256        if de.eq_ignore_ascii_case(DE_PLASMA) {
257            detect_plasma(&mut result);
258        } else if de.eq_ignore_ascii_case(DE_LXQT) {
259            detect_lxqt(&mut result);
260        }
261    }
262
263    if result.widget_style.is_empty() && let Ok(qpa) = std::env::var("QT_QPA_PLATFORMTHEME") && ((qpa == "qt5ct") || (qpa == "qt6ct")) {
264        let qver: char = qpa.chars().nth(2).unwrap_or('5');
265        detect_qtct(qver, &mut result);
266    }
267
268    if (result.widget_style == "kvantum") || (result.widget_style == "kvantum-dark") {
269        (&mut result).widget_style.clear();
270        detect_kvantum(&mut result);
271    }
272
273    result
274}