x11rb/cursor/
find_cursor.rs

1//! Find the right cursor file from a cursor name
2
3// Based on libxcb-cursor's load_cursor.c which has:
4//
5//   Copyright © 2013 Michael Stapelberg
6//   Copyright © 2002 Keith Packard
7//
8// and is licensed under MIT/X Consortium License
9
10use std::env::{var, var_os};
11use std::ffi::OsStr;
12use std::fs::File;
13use std::io::{BufRead, BufReader, Error as IOError};
14use std::path::{Path, PathBuf};
15
16static CORE_CURSORS: &[(&str, u16)] = &[
17    ("X_cursor", 0),
18    ("arrow", 1),
19    ("based_arrow_down", 2),
20    ("based_arrow_up", 3),
21    ("boat", 4),
22    ("bogosity", 5),
23    ("bottom_left_corner", 6),
24    ("bottom_right_corner", 7),
25    ("bottom_side", 8),
26    ("bottom_tee", 9),
27    ("box_spiral", 10),
28    ("center_ptr", 11),
29    ("circle", 12),
30    ("clock", 13),
31    ("coffee_mug", 14),
32    ("cross", 15),
33    ("cross_reverse", 16),
34    ("crosshair", 17),
35    ("diamond_cross", 18),
36    ("dot", 19),
37    ("dotbox", 20),
38    ("double_arrow", 21),
39    ("draft_large", 22),
40    ("draft_small", 23),
41    ("draped_box", 24),
42    ("exchange", 25),
43    ("fleur", 26),
44    ("gobbler", 27),
45    ("gumby", 28),
46    ("hand1", 29),
47    ("hand2", 30),
48    ("heart", 31),
49    ("icon", 32),
50    ("iron_cross", 33),
51    ("left_ptr", 34),
52    ("left_side", 35),
53    ("left_tee", 36),
54    ("leftbutton", 37),
55    ("ll_angle", 38),
56    ("lr_angle", 39),
57    ("man", 40),
58    ("middlebutton", 41),
59    ("mouse", 42),
60    ("pencil", 43),
61    ("pirate", 44),
62    ("plus", 45),
63    ("question_arrow", 46),
64    ("right_ptr", 47),
65    ("right_side", 48),
66    ("right_tee", 49),
67    ("rightbutton", 50),
68    ("rtl_logo", 51),
69    ("sailboat", 52),
70    ("sb_down_arrow", 53),
71    ("sb_h_double_arrow", 54),
72    ("sb_left_arrow", 55),
73    ("sb_right_arrow", 56),
74    ("sb_up_arrow", 57),
75    ("sb_v_double_arrow", 58),
76    ("shuttle", 59),
77    ("sizing", 60),
78    ("spider", 61),
79    ("spraycan", 62),
80    ("star", 63),
81    ("target", 64),
82    ("tcross", 65),
83    ("top_left_arrow", 66),
84    ("top_left_corner", 67),
85    ("top_right_corner", 68),
86    ("top_side", 69),
87    ("top_tee", 70),
88    ("trek", 71),
89    ("ul_angle", 72),
90    ("umbrella", 73),
91    ("ur_angle", 74),
92    ("watch", 75),
93    ("xterm", 76),
94];
95
96/// Find a core cursor based on its name
97///
98/// This function checks a built-in list of known names.
99fn cursor_shape_to_id(name: &str) -> Option<u16> {
100    CORE_CURSORS
101        .iter()
102        .filter(|&(name2, _)| name == *name2)
103        .map(|&(_, id)| id)
104        .next()
105}
106
107/// An error that occurred while searching
108#[derive(Debug)]
109pub(crate) enum Error {
110    /// `$HOME` is not set
111    NoHomeDir,
112
113    /// No cursor file could be found
114    NothingFound,
115}
116
117/// The result of finding a cursor
118#[derive(Debug)]
119pub(crate) enum Cursor<F> {
120    /// The cursor is a core cursor that can be created with xproto's `CreateGlyphCursor`
121    CoreChar(u16),
122
123    /// A cursor file was opened
124    File(F),
125}
126
127// Get the 'Inherits' entry from an index.theme file
128fn parse_inherits(filename: &Path) -> Result<Vec<String>, IOError> {
129    let file = File::open(filename)?;
130    parse_inherits_impl(&mut BufReader::new(file))
131}
132
133// Get the 'Inherits' entry from an index.theme file
134fn parse_inherits_impl(input: &mut impl BufRead) -> Result<Vec<String>, IOError> {
135    let mut buffer = Vec::new();
136    loop {
137        buffer.clear();
138
139        // Read a line
140        if 0 == input.read_until(b'\n', &mut buffer)? {
141            // End of file, return an empty result
142            return Ok(Default::default());
143        }
144
145        // Remove end of line marker
146        if buffer.last() == Some(&b'\n') {
147            let _ = buffer.pop();
148        }
149
150        let begin = b"Inherits";
151        if buffer.starts_with(begin) {
152            let mut result = Vec::new();
153
154            let mut to_parse = &buffer[begin.len()..];
155
156            fn skip_while(mut slice: &[u8], f: impl Fn(u8) -> bool) -> &[u8] {
157                while !slice.is_empty() && f(slice[0]) {
158                    slice = &slice[1..]
159                }
160                slice
161            }
162
163            // Skip all spaces
164            to_parse = skip_while(to_parse, |c| c == b' ');
165
166            // Now we need an equal sign
167            if to_parse.first() == Some(&b'=') {
168                to_parse = &to_parse[1..];
169
170                fn should_skip(c: u8) -> bool {
171                    matches!(c, b' ' | b'\t' | b'\n' | b';' | b',')
172                }
173
174                // Iterate over the pieces
175                for mut part in to_parse.split(|&x| x == b':') {
176                    // Skip all leading whitespace
177                    part = skip_while(part, should_skip);
178
179                    // Skip all trailing whitespace
180                    while let Some((&last, rest)) = part.split_last() {
181                        if !should_skip(last) {
182                            break;
183                        }
184                        part = rest;
185                    }
186                    if !part.is_empty() {
187                        if let Ok(part) = std::str::from_utf8(part) {
188                            result.push(part.to_string());
189                        }
190                    }
191                }
192            }
193            return Ok(result);
194        }
195    }
196}
197
198#[cfg(test)]
199mod test_parse_inherits {
200    use super::parse_inherits_impl;
201    use std::io::Cursor;
202
203    #[test]
204    fn parse_inherits_successful() {
205        let input =
206            b"Hi\nInherits = \t ; whatever ;,::; stuff : i s ,: \tthis \t \nInherits=ignored\n";
207        let mut input = Cursor::new(&input[..]);
208        let result = parse_inherits_impl(&mut input).unwrap();
209        assert_eq!(result, vec!["whatever", "stuff", "i s", "this"]);
210    }
211}
212
213/// Find a cursor file based on the name of a cursor theme and the name of the cursor.
214pub(crate) fn find_cursor(theme: &str, name: &str) -> Result<Cursor<File>, Error> {
215    let home = match var_os("HOME") {
216        Some(home) => home,
217        None => return Err(Error::NoHomeDir),
218    };
219    let cursor_path = var("XCURSOR_PATH").unwrap_or_else(|_| {
220        "~/.icons:/usr/share/icons:/usr/share/pixmaps:/usr/X11R6/lib/X11/icons".into()
221    });
222    let open_cursor = |file: &Path| File::open(file);
223    find_cursor_impl(
224        &home,
225        &cursor_path,
226        theme,
227        name,
228        open_cursor,
229        parse_inherits,
230    )
231}
232
233fn find_cursor_impl<F, G, H>(
234    home: &OsStr,
235    cursor_path: &str,
236    theme: &str,
237    name: &str,
238    mut open_cursor: G,
239    mut parse_inherits: H,
240) -> Result<Cursor<F>, Error>
241where
242    G: FnMut(&Path) -> Result<F, IOError>,
243    H: FnMut(&Path) -> Result<Vec<String>, IOError>,
244{
245    if theme == "core" {
246        if let Some(id) = cursor_shape_to_id(name) {
247            return Ok(Cursor::CoreChar(id));
248        }
249    }
250
251    let cursor_path = cursor_path.split(':').collect::<Vec<_>>();
252
253    let mut next_inherits = Vec::new();
254    let mut last_inherits = vec![theme.into()];
255    while !last_inherits.is_empty() {
256        for theme in last_inherits {
257            for path in &cursor_path {
258                // Calculate the path to the theme's directory
259                let mut theme_dir = PathBuf::new();
260                // Does the path begin with '~'?
261                if let Some(mut path) = path.strip_prefix('~') {
262                    theme_dir.push(home);
263                    // Skip a path separator if there is one
264                    if path.chars().next().map(std::path::is_separator) == Some(true) {
265                        path = &path[1..];
266                    }
267                    theme_dir.push(path);
268                } else {
269                    theme_dir.push(path);
270                }
271                theme_dir.push(&theme);
272
273                // Find the cursor in the theme
274                let mut cursor_file = theme_dir.clone();
275                cursor_file.push("cursors");
276                cursor_file.push(name);
277                if let Ok(file) = open_cursor(&cursor_file) {
278                    return Ok(Cursor::File(file));
279                }
280
281                // Get the theme's index.theme file and parse its 'Inherits' line
282                let mut index = theme_dir;
283                index.push("index.theme");
284                if let Ok(res) = parse_inherits(&index) {
285                    next_inherits.extend(res);
286                }
287            }
288        }
289
290        last_inherits = next_inherits;
291        next_inherits = Vec::new();
292    }
293
294    Err(Error::NothingFound)
295}
296
297// FIXME: Make these tests pass on Windows; problem is "/" vs "\\" in paths
298#[cfg(all(test, unix))]
299mod test_find_cursor {
300    use super::{find_cursor_impl, Cursor, Error};
301    use crate::errors::ConnectionError;
302    use std::io::{Error as IOError, ErrorKind};
303    use std::path::Path;
304
305    #[test]
306    fn core_cursor() {
307        let cb1 = |_: &Path| -> Result<(), _> { unimplemented!() };
308        let cb2 = |_: &Path| unimplemented!();
309        match find_cursor_impl("unused".as_ref(), "unused", "core", "heart", cb1, cb2).unwrap() {
310            Cursor::CoreChar(31) => {}
311            e => panic!("Unexpected result {:?}", e),
312        }
313    }
314
315    #[test]
316    fn nothing_found() {
317        let mut opened = Vec::new();
318        let mut inherit_parsed = Vec::new();
319        let cb1 = |path: &Path| -> Result<(), _> {
320            opened.push(path.to_str().unwrap().to_owned());
321            Err(IOError::new(
322                ErrorKind::Other,
323                ConnectionError::UnknownError,
324            ))
325        };
326        let cb2 = |path: &Path| {
327            inherit_parsed.push(path.to_str().unwrap().to_owned());
328            Ok(Vec::new())
329        };
330        match find_cursor_impl(
331            "home".as_ref(),
332            "path:~/some/:/entries",
333            "theme",
334            "theCursor",
335            cb1,
336            cb2,
337        ) {
338            Err(Error::NothingFound) => {}
339            e => panic!("Unexpected result {:?}", e),
340        }
341        assert_eq!(
342            opened,
343            &[
344                "path/theme/cursors/theCursor",
345                "home/some/theme/cursors/theCursor",
346                "/entries/theme/cursors/theCursor",
347            ]
348        );
349        assert_eq!(
350            inherit_parsed,
351            &[
352                "path/theme/index.theme",
353                "home/some/theme/index.theme",
354                "/entries/theme/index.theme",
355            ]
356        );
357    }
358
359    #[test]
360    fn inherit() {
361        let mut opened = Vec::new();
362        let cb1 = |path: &Path| -> Result<(), _> {
363            opened.push(path.to_str().unwrap().to_owned());
364            Err(IOError::new(
365                ErrorKind::Other,
366                ConnectionError::UnknownError,
367            ))
368        };
369        let cb2 = |path: &Path| {
370            if path.starts_with("base/theTheme") {
371                Ok(vec!["inherited".into()])
372            } else if path.starts_with("path/inherited") {
373                Ok(vec!["theEnd".into()])
374            } else {
375                Ok(vec![])
376            }
377        };
378        match find_cursor_impl(
379            "home".as_ref(),
380            "path:base:tail",
381            "theTheme",
382            "theCursor",
383            cb1,
384            cb2,
385        ) {
386            Err(Error::NothingFound) => {}
387            e => panic!("Unexpected result {:?}", e),
388        }
389        assert_eq!(
390            opened,
391            &[
392                "path/theTheme/cursors/theCursor",
393                "base/theTheme/cursors/theCursor",
394                "tail/theTheme/cursors/theCursor",
395                "path/inherited/cursors/theCursor",
396                "base/inherited/cursors/theCursor",
397                "tail/inherited/cursors/theCursor",
398                "path/theEnd/cursors/theCursor",
399                "base/theEnd/cursors/theCursor",
400                "tail/theEnd/cursors/theCursor",
401            ]
402        );
403    }
404}