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(icon_name, &self.search_paths, &mut walked_themes) |
42 | } |
43 | } |
44 | |
45 | #[derive (Debug, PartialEq, Eq, Clone)] |
46 | struct 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 | |
54 | impl 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. |
131 | fn 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. |
204 | fn 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. |
211 | fn 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)] |
246 | mod 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 | |