1//! A crate to load cursor themes, and parse XCursor files.
2
3use std::collections::HashSet;
4use std::env;
5use std::path::{Path, PathBuf};
6
7/// A module implementing XCursor file parsing.
8pub mod parser;
9
10/// A cursor theme.
11#[derive(Debug, PartialEq, Eq, Clone)]
12pub struct CursorTheme {
13 theme: CursorThemeIml,
14 /// Global search path for themes.
15 search_paths: Vec<PathBuf>,
16}
17
18impl CursorTheme {
19 /// Search for a theme with the given name in the given search paths,
20 /// and returns an XCursorTheme which represents it. If no inheritance
21 /// can be determined, then the themes inherits from the "default" theme.
22 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 /// Try to load an icon from the theme.
34 /// If the icon is not found within this theme's
35 /// directories, then the function looks at the
36 /// theme from which this theme is inherited.
37 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 /// Try to load an icon from the theme, returning it with its inheritance
46 /// depth.
47 ///
48 /// If the icon is not found within this theme's directories, then the
49 /// function looks at the theme from which this theme is inherited. The
50 /// second element of the returned tuple indicates how many levels of
51 /// inheritance were traversed before the icon was found.
52 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 /// Theme name.
63 name: String,
64 /// Directories where the theme is presented and corresponding names of inherited themes.
65 /// `None` if theme inherits nothing.
66 data: Vec<(PathBuf, Option<String>)>,
67}
68
69impl CursorThemeIml {
70 /// The implementation of cursor theme loading.
71 fn load(name: &str, search_paths: &[PathBuf]) -> Self {
72 let mut data = Vec::new();
73
74 // Find directories where this theme is presented.
75 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 /// The implementation of cursor icon loading.
100 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 // We've processed all based theme files. Traverse inherited themes, marking this theme
116 // as already visited to avoid infinite recursion.
117 walked_themes.insert(self.name.clone());
118
119 for data in &self.data {
120 // Get inherited theme name, if any.
121 let inherits = match data.1.as_ref() {
122 Some(inherits) => inherits,
123 None => continue,
124 };
125
126 // We've walked this theme, avoid rebuilding.
127 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
143/// Get the list of paths where the themes have to be searched,
144/// according to the XDG Icon Theme specification, respecting `XCURSOR_PATH` env
145/// variable, in case it was set.
146fn theme_search_paths() -> Vec<PathBuf> {
147 // Handle the `XCURSOR_PATH` env variable, which takes over default search paths for cursor
148 // theme. Some systems rely are using non standard directory layout and primary using this
149 // env variable to perform cursor loading from a right places.
150 let xcursor_path = match env::var("XCURSOR_PATH") {
151 Ok(xcursor_path) => xcursor_path.split(':').map(PathBuf::from).collect(),
152 Err(_) => {
153 // Get icons locations from XDG data directories.
154 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 // The order is following other XCursor loading libs, like libwayland-cursor.
178 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 // Replace `~` in a path with `$HOME` for compatibility with other libs.
195 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
215/// Load the specified index.theme file, and returns a `Some` with
216/// the value of the `Inherits` key in it.
217/// Returns `None` if the file cannot be read for any reason,
218/// if the file cannot be parsed, or if the `Inherits` key is omitted.
219fn theme_inherits(file_path: &Path) -> Option<String> {
220 let content: String = std::fs::read_to_string(file_path).ok()?;
221
222 parse_theme(&content)
223}
224
225/// Parse the content of the `index.theme` and return the `Inherits` value.
226fn 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 // Line should start with `Inherits`, otherwise go to the next line.
234 if !line.starts_with(PATTERN) {
235 continue;
236 }
237
238 // Skip the `Inherits` part and trim the leading white spaces.
239 let mut chars = line.get(PATTERN.len()..).unwrap().trim_start().chars();
240
241 // If the next character after leading white spaces isn't `=` go the next line.
242 if Some('=') != chars.next() {
243 continue;
244 }
245
246 // Skip XCursor spaces/separators.
247 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}
299