1use ini::Ini;
2use std::{path::PathBuf, sync::LazyLock};
3
4static CURRENT_ICON_THEME: LazyLock<IconTheme> = LazyLock::new(IconTheme::current);
5
6#[derive(Debug, Clone)]
7pub struct IconTheme {
8 name: String,
9 path: PathBuf,
10 config: Ini,
11}
12
13impl IconTheme {
14 pub fn name(&self) -> &str {
15 &self.name
16 }
17
18 pub fn path(&self) -> PathBuf {
19 self.path.clone()
20 }
21
22 fn config(&self) -> &Ini {
23 &self.config
24 }
25
26 pub fn config_value<S: Into<String>, A: AsRef<str>>(
27 &self,
28 section_name: S,
29 key: A,
30 ) -> Option<String> {
31 let cfg = self.config();
32 let section = cfg.section(Some(section_name))?;
33 let value = section.get(key)?;
34
35 Some(value.to_string())
36 }
37
38 pub fn inherits(&self) -> Vec<String> {
39 let Some(inherits) = &self.config_value("Icon Theme", "Inherits") else {
40 return Vec::new();
41 };
42
43 inherits.split(",").map(String::from).collect()
44 }
45
46 pub fn icon_dirs(&self, size: u32, scale: u8) -> Vec<PathBuf> {
52 let Some(dir_str) = &self.config_value("Icon Theme", "Directories") else {
53 return Vec::new();
54 };
55
56 let dirs: Vec<String> = dir_str.split(",").map(String::from).collect();
57 let overlay_dir = freedesktop_core::xdg_data_home()
58 .join("icons")
59 .join(&self.name);
60
61 let mut paths: Vec<PathBuf> = Vec::new();
62
63 for d in &dirs {
64 let dir_size = &self
65 .config_value(d, "Size")
66 .map(|s| s.parse::<u32>().unwrap_or(0))
67 .unwrap_or(0);
68
69 let dir_scale = match &self.config_value(d, "Scale") {
70 Some(s) => s.parse::<u8>().unwrap_or(1),
71 None => 1,
72 };
73
74 if (dir_size == &size) && (dir_scale == scale) {
75 let overlay = overlay_dir.join(d);
76 if overlay.exists() {
77 paths.push(overlay);
78 }
79
80 paths.push(self.path.join(d));
81 }
82 }
83
84 paths
85 }
86
87 pub fn default_size(&self) -> Option<u32> {
88 let Some(size_str) = &self.config_value("Icon Theme", "DesktopDefault") else {
89 return None;
90 };
91
92 size_str.parse::<u32>().ok()
93 }
94
95 fn get_icon(&self, icon_name: &str, size: u32, scale: u8) -> Option<PathBuf> {
99 let filenames = [
100 format!("{}.{}", icon_name, "svg"),
101 format!("{}.{}", icon_name, "png"),
102 format!("{}.{}", icon_name, "xpm"),
103 ];
104
105 for d in &self.icon_dirs(size, scale) {
106 for f in &filenames {
107 let icon_path = d.join(f);
108 if icon_path.exists() {
109 return Some(icon_path);
110 }
111 }
112 }
113
114 None
115 }
116
117 fn get_through_inheritance(&self, icon_name: &str, size: u32, scale: u8) -> Option<PathBuf> {
121 if let Some(icon_path) = &self.get_icon(icon_name, size, scale) {
122 return Some(icon_path.to_owned());
123 }
124
125 for theme_name in &self.inherits() {
128 let Some(theme) = IconTheme::from_name(theme_name) else {
129 continue;
130 };
131
132 match theme.get_through_inheritance(icon_name, size, scale) {
133 Some(icon_path) => return Some(icon_path),
134 None => continue,
135 }
136 }
137
138 None
139 }
140
141 pub fn get(&self, icon_name: &str) -> Option<PathBuf> {
144 let size = self.default_size().unwrap_or(48);
145 let scale: u8 = 1;
146
147 if let Some(path) = &self.get_through_inheritance(icon_name, size, scale) {
148 return Some(path.to_owned());
149 }
150
151 Pixmap::get(icon_name)
153 }
154}
155
156impl IconTheme {
157 pub fn from_name<S: Into<String>>(name: S) -> Option<IconTheme> {
166 let name: String = name.into();
167 let xdg_home_path = freedesktop_core::xdg_data_home().join("icons").join(&name);
168
169 if xdg_home_path.exists() {
170 let config_path = xdg_home_path.join("index.theme");
171 if config_path.exists() {
172 let config = Ini::load_from_file(&config_path).unwrap_or_else(|_| Ini::new());
173 return Some(IconTheme {
174 name,
175 path: xdg_home_path,
176 config,
177 });
178 }
179 }
180
181 for data_dir in freedesktop_core::xdg_data_dirs() {
182 let theme_path = data_dir.join("icons").join(&name);
183
184 if theme_path.exists() {
185 let config_path = theme_path.join("index.theme");
186 if config_path.exists() {
187 let config = Ini::load_from_file(&config_path).unwrap_or_else(|_| Ini::new());
188 return Some(IconTheme {
189 name,
190 path: theme_path,
191 config,
192 });
193 }
194 }
195 }
196
197 None
198 }
199
200 pub fn current() -> IconTheme {
201 let home = std::env::var("HOME").expect("$HOME variable not set.");
202 let config_path = freedesktop_core::xdg_config_home();
203 let settings_paths = [
204 PathBuf::from(&config_path)
205 .join("gtk-4.0")
206 .join("settings.ini"),
207 PathBuf::from(&config_path)
208 .join("gtk-3.0")
209 .join("settings.ini"),
210 PathBuf::from(&home).join("gtk-4.0").join("settings.ini"),
211 PathBuf::from(&home).join("gtk-3.0").join("settings.ini"),
212 ];
213 let fallback_theme = || {
214 IconTheme::from_name("hicolor").expect("The hicolor theme is not present. This is a required fallback theme and must be installed")
215 };
216
217 for p in &settings_paths {
218 if !p.exists() {
219 continue;
220 }
221
222 let Ok(conf) = Ini::load_from_file(p) else {
223 continue;
224 };
225
226 if let Some(section) = conf.section(Some("Settings")) {
227 if let Some(theme) = section.get("gtk-icon-theme-name") {
228 return IconTheme::from_name(theme).unwrap_or_else(fallback_theme);
229 } else {
230 continue;
231 }
232 }
233 }
234
235 fallback_theme()
236 }
237}
238
239pub struct Pixmap;
242
243impl Pixmap {
244 pub fn get(icon_name: &str) -> Option<PathBuf> {
245 let pixmap_paths = freedesktop_core::xdg_data_dirs()
246 .into_iter()
247 .map(|p| p.join("pixmaps"))
248 .filter(|p| p.exists());
249
250 let filenames = [
251 format!("{}.{}", icon_name, "svg"),
252 format!("{}.{}", icon_name, "png"),
253 format!("{}.{}", icon_name, "xpm"),
254 ];
255
256 for d in pixmap_paths {
257 for f in &filenames {
258 let icon_path = d.join(f);
259 if icon_path.exists() {
260 return Some(icon_path);
261 }
262 }
263 }
264
265 None
266 }
267}
268
269pub fn get_icon(name: &str) -> Option<PathBuf> {
277 CURRENT_ICON_THEME.get(name)
278}