1 | //! A crate to load cursor themes, and parse XCursor files. |
2 | |
3 | use std::collections::HashSet; |
4 | use std::env; |
5 | use std::path::{Path, PathBuf}; |
6 | |
7 | /// A module implementing XCursor file parsing. |
8 | pub mod parser; |
9 | |
10 | /// A cursor theme. |
11 | #[derive (Debug, PartialEq, Eq, Clone)] |
12 | pub struct CursorTheme { |
13 | theme: CursorThemeIml, |
14 | /// Global search path for themes. |
15 | search_paths: Vec<PathBuf>, |
16 | } |
17 | |
18 | impl 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)] |
61 | struct 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 | |
69 | impl 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. |
146 | fn 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. |
219 | fn 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. |
226 | fn 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)] |
261 | mod 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 | |