xcursor/
lib.rs

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 = 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}