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(icon_name, &self.search_paths, &mut walked_themes)
42 }
43}
44
45#[derive(Debug, PartialEq, Eq, Clone)]
46struct CursorThemeIml {
47 /// Theme name.
48 name: String,
49 /// Directories where the theme is presented and corresponding names of inherited themes.
50 /// `None` if theme inherits nothing.
51 data: Vec<(PathBuf, Option<String>)>,
52}
53
54impl CursorThemeIml {
55 /// The implementation of cursor theme loading.
56 fn load(name: &str, search_paths: &[PathBuf]) -> Self {
57 let mut data = Vec::new();
58
59 // Find directories where this theme is presented.
60 for mut path in search_paths.iter().cloned() {
61 path.push(name);
62 if path.is_dir() {
63 let data_dir = path.clone();
64
65 path.push("index.theme");
66 let inherits = if let Some(inherits) = theme_inherits(&path) {
67 Some(inherits)
68 } else if name != "default" {
69 Some(String::from("default"))
70 } else {
71 None
72 };
73
74 data.push((data_dir, inherits));
75 }
76 }
77
78 CursorThemeIml {
79 name: name.to_owned(),
80 data,
81 }
82 }
83
84 /// The implementation of cursor icon loading.
85 fn load_icon(
86 &self,
87 icon_name: &str,
88 search_paths: &[PathBuf],
89 walked_themes: &mut HashSet<String>,
90 ) -> Option<PathBuf> {
91 for data in &self.data {
92 let mut icon_path = data.0.clone();
93 icon_path.push("cursors");
94 icon_path.push(icon_name);
95 if icon_path.is_file() {
96 return Some(icon_path);
97 }
98 }
99
100 // We've processed all based theme files. Traverse inherited themes, marking this theme
101 // as already visited to avoid infinite recursion.
102 walked_themes.insert(self.name.clone());
103
104 for data in &self.data {
105 // Get inherited theme name, if any.
106 let inherits = match data.1.as_ref() {
107 Some(inherits) => inherits,
108 None => continue,
109 };
110
111 // We've walked this theme, avoid rebuilding.
112 if walked_themes.contains(inherits) {
113 continue;
114 }
115
116 let inherited_theme = CursorThemeIml::load(inherits, search_paths);
117
118 match inherited_theme.load_icon(icon_name, search_paths, walked_themes) {
119 Some(icon_path) => return Some(icon_path),
120 None => continue,
121 }
122 }
123
124 None
125 }
126}
127
128/// Get the list of paths where the themes have to be searched,
129/// according to the XDG Icon Theme specification, respecting `XCURSOR_PATH` env
130/// variable, in case it was set.
131fn theme_search_paths() -> Vec<PathBuf> {
132 // Handle the `XCURSOR_PATH` env variable, which takes over default search paths for cursor
133 // theme. Some systems rely are using non standard directory layout and primary using this
134 // env variable to perform cursor loading from a right places.
135 let xcursor_path = match env::var("XCURSOR_PATH") {
136 Ok(xcursor_path) => xcursor_path.split(':').map(PathBuf::from).collect(),
137 Err(_) => {
138 // Get icons locations from XDG data directories.
139 let get_icon_dirs = |xdg_path: String| -> Vec<PathBuf> {
140 xdg_path
141 .split(':')
142 .map(|entry| {
143 let mut entry = PathBuf::from(entry);
144 entry.push("icons");
145 entry
146 })
147 .collect()
148 };
149
150 let mut xdg_data_home = get_icon_dirs(
151 env::var("XDG_DATA_HOME").unwrap_or_else(|_| String::from("~/.local/share")),
152 );
153
154 let mut xdg_data_dirs = get_icon_dirs(
155 env::var("XDG_DATA_DIRS")
156 .unwrap_or_else(|_| String::from("/usr/local/share:/usr/share")),
157 );
158
159 let mut xcursor_path =
160 Vec::with_capacity(xdg_data_dirs.len() + xdg_data_home.len() + 4);
161
162 // The order is following other XCursor loading libs, like libwayland-cursor.
163 xcursor_path.append(&mut xdg_data_home);
164 xcursor_path.push(PathBuf::from("~/.icons"));
165 xcursor_path.append(&mut xdg_data_dirs);
166 xcursor_path.push(PathBuf::from("/usr/share/pixmaps"));
167 xcursor_path.push(PathBuf::from("~/.cursors"));
168 xcursor_path.push(PathBuf::from("/usr/share/cursors/xorg-x11"));
169
170 xcursor_path
171 }
172 };
173
174 let homedir = env::var("HOME");
175
176 xcursor_path
177 .into_iter()
178 .filter_map(|dir| {
179 // Replace `~` in a path with `$HOME` for compatibility with other libs.
180 let mut expaned_dir = PathBuf::new();
181
182 for component in dir.iter() {
183 if component == "~" {
184 let homedir = match homedir.as_ref() {
185 Ok(homedir) => homedir.clone(),
186 Err(_) => return None,
187 };
188
189 expaned_dir.push(homedir);
190 } else {
191 expaned_dir.push(component);
192 }
193 }
194
195 Some(expaned_dir)
196 })
197 .collect()
198}
199
200/// Load the specified index.theme file, and returns a `Some` with
201/// the value of the `Inherits` key in it.
202/// Returns `None` if the file cannot be read for any reason,
203/// if the file cannot be parsed, or if the `Inherits` key is omitted.
204fn theme_inherits(file_path: &Path) -> Option<String> {
205 let content: String = std::fs::read_to_string(file_path).ok()?;
206
207 parse_theme(&content)
208}
209
210/// Parse the content of the `index.theme` and return the `Inherits` value.
211fn parse_theme(content: &str) -> Option<String> {
212 const PATTERN: &str = "Inherits";
213
214 let is_xcursor_space_or_separator =
215 |&ch: &char| -> bool { ch.is_whitespace() || ch == ';' || ch == ',' };
216
217 for line in content.lines() {
218 // Line should start with `Inherits`, otherwise go to the next line.
219 if !line.starts_with(PATTERN) {
220 continue;
221 }
222
223 // Skip the `Inherits` part and trim the leading white spaces.
224 let mut chars = line.get(PATTERN.len()..).unwrap().trim_start().chars();
225
226 // If the next character after leading white spaces isn't `=` go the next line.
227 if Some('=') != chars.next() {
228 continue;
229 }
230
231 // Skip XCursor spaces/separators.
232 let result: String = chars
233 .skip_while(is_xcursor_space_or_separator)
234 .take_while(|ch| !is_xcursor_space_or_separator(ch))
235 .collect();
236
237 if !result.is_empty() {
238 return Some(result);
239 }
240 }
241
242 None
243}
244
245#[cfg(test)]
246mod tests {
247 use super::parse_theme;
248
249 #[test]
250 fn parse_inherits() {
251 let theme_name = String::from("XCURSOR_RS");
252
253 let theme = format!("Inherits={}", theme_name.clone());
254
255 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
256
257 let theme = format!(" Inherits={}", theme_name.clone());
258
259 assert_eq!(parse_theme(&theme), None);
260
261 let theme = format!(
262 "[THEME name]\nInherits = ,;\t\t{};;;;Tail\n\n",
263 theme_name.clone()
264 );
265
266 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
267
268 let theme = format!("Inherits;=;{}", theme_name.clone());
269
270 assert_eq!(parse_theme(&theme), None);
271
272 let theme = format!("Inherits = {}\n\nInherits=OtherTheme", theme_name.clone());
273
274 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
275
276 let theme = format!(
277 "Inherits = ;;\nSome\tgarbage\nInherits={}",
278 theme_name.clone()
279 );
280
281 assert_eq!(parse_theme(&theme), Some(theme_name.clone()));
282 }
283}
284