1use std::collections::HashSet;
4use std::env;
5use std::path::{Path, PathBuf};
6
7pub mod parser;
9
10#[derive(Debug, PartialEq, Eq, Clone)]
12pub struct CursorTheme {
13 theme: CursorThemeIml,
14 search_paths: Vec<PathBuf>,
16}
17
18impl CursorTheme {
19 pub fn load(name: &str) -> Self {
23 let search_paths = theme_search_paths();
24
25 let theme = CursorThemeIml::load(name, &search_paths);
26
27 CursorTheme {
28 theme,
29 search_paths,
30 }
31 }
32
33 pub fn load_icon(&self, icon_name: &str) -> Option<PathBuf> {
38 let mut walked_themes = HashSet::new();
39
40 self.theme
41 .load_icon_with_depth(icon_name, &self.search_paths, &mut walked_themes)
42 .map(|(pathbuf, _)| pathbuf)
43 }
44
45 pub fn load_icon_with_depth(&self, icon_name: &str) -> Option<(PathBuf, usize)> {
53 let mut walked_themes = HashSet::new();
54
55 self.theme
56 .load_icon_with_depth(icon_name, &self.search_paths, &mut walked_themes)
57 }
58}
59
60#[derive(Debug, PartialEq, Eq, Clone)]
61struct CursorThemeIml {
62 name: String,
64 data: Vec<(PathBuf, Option<String>)>,
67}
68
69impl CursorThemeIml {
70 fn load(name: &str, search_paths: &[PathBuf]) -> Self {
72 let mut data = Vec::new();
73
74 for mut path in search_paths.iter().cloned() {
76 path.push(name);
77 if path.is_dir() {
78 let data_dir = path.clone();
79
80 path.push("index.theme");
81 let inherits = if let Some(inherits) = theme_inherits(&path) {
82 Some(inherits)
83 } else if name != "default" {
84 Some(String::from("default"))
85 } else {
86 None
87 };
88
89 data.push((data_dir, inherits));
90 }
91 }
92
93 CursorThemeIml {
94 name: name.to_owned(),
95 data,
96 }
97 }
98
99 fn load_icon_with_depth(
101 &self,
102 icon_name: &str,
103 search_paths: &[PathBuf],
104 walked_themes: &mut HashSet<String>,
105 ) -> Option<(PathBuf, usize)> {
106 for data in &self.data {
107 let mut icon_path = data.0.clone();
108 icon_path.push("cursors");
109 icon_path.push(icon_name);
110 if icon_path.is_file() {
111 return Some((icon_path, 0));
112 }
113 }
114
115 walked_themes.insert(self.name.clone());
118
119 for data in &self.data {
120 let inherits = match data.1.as_ref() {
122 Some(inherits) => inherits,
123 None => continue,
124 };
125
126 if walked_themes.contains(inherits) {
128 continue;
129 }
130
131 let inherited_theme = CursorThemeIml::load(inherits, search_paths);
132
133 match inherited_theme.load_icon_with_depth(icon_name, search_paths, walked_themes) {
134 Some((icon_path, depth)) => return Some((icon_path, depth + 1)),
135 None => continue,
136 }
137 }
138
139 None
140 }
141}
142
143fn theme_search_paths() -> Vec<PathBuf> {
147 let xcursor_path = match env::var("XCURSOR_PATH") {
151 Ok(xcursor_path) => xcursor_path.split(':').map(PathBuf::from).collect(),
152 Err(_) => {
153 let get_icon_dirs = |xdg_path: String| -> Vec<PathBuf> {
155 xdg_path
156 .split(':')
157 .map(|entry| {
158 let mut entry = PathBuf::from(entry);
159 entry.push("icons");
160 entry
161 })
162 .collect()
163 };
164
165 let mut xdg_data_home = get_icon_dirs(
166 env::var("XDG_DATA_HOME").unwrap_or_else(|_| String::from("~/.local/share")),
167 );
168
169 let mut xdg_data_dirs = get_icon_dirs(
170 env::var("XDG_DATA_DIRS")
171 .unwrap_or_else(|_| String::from("/usr/local/share:/usr/share")),
172 );
173
174 let mut xcursor_path =
175 Vec::with_capacity(xdg_data_dirs.len() + xdg_data_home.len() + 4);
176
177 xcursor_path.append(&mut xdg_data_home);
179 xcursor_path.push(PathBuf::from("~/.icons"));
180 xcursor_path.append(&mut xdg_data_dirs);
181 xcursor_path.push(PathBuf::from("/usr/share/pixmaps"));
182 xcursor_path.push(PathBuf::from("~/.cursors"));
183 xcursor_path.push(PathBuf::from("/usr/share/cursors/xorg-x11"));
184
185 xcursor_path
186 }
187 };
188
189 let homedir = env::var("HOME");
190
191 xcursor_path
192 .into_iter()
193 .filter_map(|dir| {
194 let mut expaned_dir = PathBuf::new();
196
197 for component in dir.iter() {
198 if component == "~" {
199 let homedir = match homedir.as_ref() {
200 Ok(homedir) => homedir.clone(),
201 Err(_) => return None,
202 };
203
204 expaned_dir.push(homedir);
205 } else {
206 expaned_dir.push(component);
207 }
208 }
209
210 Some(expaned_dir)
211 })
212 .collect()
213}
214
215fn theme_inherits(file_path: &Path) -> Option<String> {
220 let content = std::fs::read_to_string(file_path).ok()?;
221
222 parse_theme(&content)
223}
224
225fn parse_theme(content: &str) -> Option<String> {
227 const PATTERN: &str = "Inherits";
228
229 let is_xcursor_space_or_separator =
230 |&ch: &char| -> bool { ch.is_whitespace() || ch == ';' || ch == ',' };
231
232 for line in content.lines() {
233 if !line.starts_with(PATTERN) {
235 continue;
236 }
237
238 let mut chars = line.get(PATTERN.len()..).unwrap().trim_start().chars();
240
241 if Some('=') != chars.next() {
243 continue;
244 }
245
246 let result: String = chars
248 .skip_while(is_xcursor_space_or_separator)
249 .take_while(|ch| !is_xcursor_space_or_separator(ch))
250 .collect();
251
252 if !result.is_empty() {
253 return Some(result);
254 }
255 }
256
257 None
258}
259
260#[cfg(test)]
261mod tests {
262 use super::parse_theme;
263
264 #[test]
265 fn parse_inherits() {
266 let theme_name = String::from("XCURSOR_RS");
267
268 let theme = format!("Inherits={}", theme_name.clone());
269
270 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
271
272 let theme = format!(" Inherits={}", theme_name.clone());
273
274 assert_eq!(parse_theme(&theme), None);
275
276 let theme = format!(
277 "[THEME name]\nInherits = ,;\t\t{};;;;Tail\n\n",
278 theme_name.clone()
279 );
280
281 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
282
283 let theme = format!("Inherits;=;{}", theme_name.clone());
284
285 assert_eq!(parse_theme(&theme), None);
286
287 let theme = format!("Inherits = {}\n\nInherits=OtherTheme", theme_name.clone());
288
289 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
290
291 let theme = format!(
292 "Inherits = ;;\nSome\tgarbage\nInherits={}",
293 theme_name.clone()
294 );
295
296 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
297 }
298}