xcursor-0.3.4/.cargo_vcs_info.json0000644000000001120000000000100125370ustar { "git": { "sha1": "d511c171b110f538f3cbb9107a9f637b4018b847" } } xcursor-0.3.4/Cargo.toml0000644000000015110000000000100105410ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "xcursor" version = "0.3.4" authors = ["Samuele Esposito"] include = ["**/*.rs", "Cargo.toml", "LICENSE", "README.md"] description = "A library for loading XCursor themes" documentation = "https://docs.rs/xcursor" readme = "README.md" license = "MIT" repository = "https://github.com/esposm03/xcursor-rs" [dependencies.nom] version = "7.0" xcursor-0.3.4/Cargo.toml.orig000064400000000000000000000006040072674642500142540ustar 00000000000000[package] name = "xcursor" description = "A library for loading XCursor themes" version = "0.3.4" edition = "2018" authors = ["Samuele Esposito"] license = "MIT" repository = "https://github.com/esposm03/xcursor-rs" documentation = "https://docs.rs/xcursor" readme = "README.md" include = [ "**/*.rs", "Cargo.toml", "LICENSE", "README.md", ] [dependencies] nom = "7.0" xcursor-0.3.4/LICENSE000064400000000000000000000020610072674642500123710ustar 00000000000000MIT License Copyright (c) 2020 Samuele Esposito Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. xcursor-0.3.4/README.md000064400000000000000000000007160072674642500126500ustar 00000000000000# xcursor-rs [![Crates.io](https://img.shields.io/crates/v/xcursor)](https://crates.io/crates/xcursor) [![Docs.rs](https://docs.rs/xcursor/badge.svg)](https://docs.rs/xcursor/) [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) A library to load XCursor themes, and parse XCursor files. # Possible issues (to be tested) - Endianness of file is not completely clear (it's either little or native, the spec doesn't say anything about it) xcursor-0.3.4/src/lib.rs000064400000000000000000000216610072674642500132760ustar 00000000000000//! A crate to load cursor themes, and parse XCursor files. use std::collections::HashSet; use std::env; use std::path::{Path, PathBuf}; /// A module implementing XCursor file parsing. pub mod parser; /// A cursor theme. #[derive(Debug, PartialEq, Eq, Clone)] pub struct CursorTheme { theme: CursorThemeIml, /// Global search path for themes. search_paths: Vec, } impl CursorTheme { /// Search for a theme with the given name in the given search paths, /// and returns an XCursorTheme which represents it. If no inheritance /// can be determined, then the themes inherits from the "default" theme. pub fn load(name: &str) -> Self { let search_paths = theme_search_paths(); let theme = CursorThemeIml::load(name, &search_paths); CursorTheme { theme, search_paths, } } /// Try to load an icon from the theme. /// If the icon is not found within this theme's /// directories, then the function looks at the /// theme from which this theme is inherited. pub fn load_icon(&self, icon_name: &str) -> Option { let mut walked_themes = HashSet::new(); self.theme .load_icon(icon_name, &self.search_paths, &mut walked_themes) } } #[derive(Debug, PartialEq, Eq, Clone)] struct CursorThemeIml { /// Theme name. name: String, /// Directories where the theme is presented and corresponding names of inherited themes. /// `None` if theme inherits nothing. data: Vec<(PathBuf, Option)>, } impl CursorThemeIml { /// The implementation of cursor theme loading. fn load(name: &str, search_paths: &[PathBuf]) -> Self { let mut data = Vec::new(); // Find directories where this theme is presented. for mut path in search_paths.to_owned() { path.push(name); if path.is_dir() { let data_dir = path.clone(); path.push("index.theme"); let inherits = if let Some(inherits) = theme_inherits(&path) { Some(inherits) } else if name != "default" { Some(String::from("default")) } else { None }; data.push((data_dir, inherits)); } } CursorThemeIml { name: name.to_owned(), data, } } /// The implementation of cursor icon loading. fn load_icon( &self, icon_name: &str, search_paths: &[PathBuf], walked_themes: &mut HashSet, ) -> Option { for data in &self.data { let mut icon_path = data.0.clone(); icon_path.push("cursors"); icon_path.push(icon_name); if icon_path.is_file() { return Some(icon_path); } } // We've processed all based theme files. Traverse inherited themes, marking this theme // as already visited to avoid infinite recursion. walked_themes.insert(self.name.clone()); for data in &self.data { // Get inherited theme name, if any. let inherits = match data.1.as_ref() { Some(inherits) => inherits, None => continue, }; // We've walked this theme, avoid rebuilding. if walked_themes.contains(inherits) { continue; } let inherited_theme = CursorThemeIml::load(inherits, search_paths); match inherited_theme.load_icon(icon_name, search_paths, walked_themes) { Some(icon_path) => return Some(icon_path), None => continue, } } None } } /// Get the list of paths where the themes have to be searched, /// according to the XDG Icon Theme specification, respecting `XCURSOR_PATH` env /// variable, in case it was set. fn theme_search_paths() -> Vec { // Handle the `XCURSOR_PATH` env variable, which takes over default search paths for cursor // theme. Some systems rely are using non standard directory layout and primary using this // env variable to perform cursor loading from a right places. let xcursor_path = match env::var("XCURSOR_PATH") { Ok(xcursor_path) => xcursor_path.split(':').map(PathBuf::from).collect(), Err(_) => { // Get icons locations from XDG data directories. let get_icon_dirs = |xdg_path: String| -> Vec { xdg_path .split(':') .map(|entry| { let mut entry = PathBuf::from(entry); entry.push("icons"); entry }) .collect() }; let mut xdg_data_home = get_icon_dirs( env::var("XDG_DATA_HOME").unwrap_or_else(|_| String::from("~/.local/share")), ); let mut xdg_data_dirs = get_icon_dirs( env::var("XDG_DATA_DIRS") .unwrap_or_else(|_| String::from("/usr/local/share:/usr/share")), ); let mut xcursor_path = Vec::with_capacity(xdg_data_dirs.len() + xdg_data_home.len() + 4); // The order is following other XCursor loading libs, like libwayland-cursor. xcursor_path.append(&mut xdg_data_home); xcursor_path.push(PathBuf::from("~/.icons")); xcursor_path.append(&mut xdg_data_dirs); xcursor_path.push(PathBuf::from("/usr/share/pixmaps")); xcursor_path.push(PathBuf::from("~/.cursors")); xcursor_path.push(PathBuf::from("/usr/share/cursors/xorg-x11")); xcursor_path } }; let homedir = env::var("HOME"); xcursor_path .into_iter() .filter_map(|dir| { // Replace `~` in a path with `$HOME` for compatibility with other libs. let mut expaned_dir = PathBuf::new(); for component in dir.iter() { if component == "~" { let homedir = match homedir.as_ref() { Ok(homedir) => homedir.clone(), Err(_) => return None, }; expaned_dir.push(homedir); } else { expaned_dir.push(component); } } Some(expaned_dir) }) .collect() } /// Load the specified index.theme file, and returns a `Some` with /// the value of the `Inherits` key in it. /// Returns `None` if the file cannot be read for any reason, /// if the file cannot be parsed, or if the `Inherits` key is omitted. fn theme_inherits(file_path: &Path) -> Option { let content = std::fs::read_to_string(file_path).ok()?; parse_theme(&content) } /// Parse the content of the `index.theme` and return the `Inherits` value. fn parse_theme(content: &str) -> Option { const PATTERN: &str = "Inherits"; let is_xcursor_space_or_separator = |&ch: &char| -> bool { ch.is_whitespace() || ch == ';' || ch == ',' }; for line in content.lines() { // Line should start with `Inherits`, otherwise go to the next line. if !line.starts_with(PATTERN) { continue; } // Skip the `Inherits` part and trim the leading white spaces. let mut chars = line.get(PATTERN.len()..).unwrap().trim_start().chars(); // If the next character after leading white spaces isn't `=` go the next line. if Some('=') != chars.next() { continue; } // Skip XCursor spaces/separators. let result: String = chars .skip_while(is_xcursor_space_or_separator) .take_while(|ch| !is_xcursor_space_or_separator(ch)) .collect(); if !result.is_empty() { return Some(result); } } None } #[cfg(test)] mod tests { use super::parse_theme; #[test] fn parse_inherits() { let theme_name = String::from("XCURSOR_RS"); let theme = format!("Inherits={}", theme_name.clone()); assert_eq!(parse_theme(&theme), Some(theme_name.clone())); let theme = format!(" Inherits={}", theme_name.clone()); assert_eq!(parse_theme(&theme), None); let theme = format!( "[THEME name]\nInherits = ,;\t\t{};;;;Tail\n\n", theme_name.clone() ); assert_eq!(parse_theme(&theme), Some(theme_name.clone())); let theme = format!("Inherits;=;{}", theme_name.clone()); assert_eq!(parse_theme(&theme), None); let theme = format!("Inherits = {}\n\nInherits=OtherTheme", theme_name.clone()); assert_eq!(parse_theme(&theme), Some(theme_name.clone())); let theme = format!( "Inherits = ;;\nSome\tgarbage\nInherits={}", theme_name.clone() ); assert_eq!(parse_theme(&theme), Some(theme_name.clone())); } } xcursor-0.3.4/src/parser.rs000064400000000000000000000140500072674642500140160ustar 00000000000000use std::{ fmt, fmt::{Debug, Formatter}, }; use nom::bytes::complete as bytes; use nom::number::complete as number; use nom::IResult; #[derive(Debug, Clone, Eq, PartialEq)] struct Toc { toctype: u32, subtype: u32, pos: u32, } /// A struct representing an image. /// Pixels are in ARGB format, with each byte representing a single channel. #[derive(Clone, Eq, PartialEq, Debug)] pub struct Image { /// The nominal size of the image. pub size: u32, /// The actual width of the image. Doesn't need to match `size`. pub width: u32, /// The actual height of the image. Doesn't need to match `size`. pub height: u32, /// The X coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated) pub xhot: u32, /// The Y coordinate of the hotspot pixel (the pixel where the tip of the arrow is situated) pub yhot: u32, /// The amount of time (in milliseconds) that this image should be shown for, before switching to the next. pub delay: u32, /// A slice containing the pixels' bytes, in RGBA format (or, in the order of the file). pub pixels_rgba: Vec, /// A slice containing the pixels' bytes, in ARGB format. pub pixels_argb: Vec, } impl std::fmt::Display for Image { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("Image") .field("size", &self.size) .field("width", &self.width) .field("height", &self.height) .field("xhot", &self.xhot) .field("yhot", &self.yhot) .field("delay", &self.delay) .field("pixels", &"/* omitted */") .finish() } } fn parse_header(i: &[u8]) -> IResult<&[u8], u32> { let (i, _) = bytes::tag("Xcur")(i)?; let (i, _) = number::le_u32(i)?; let (i, _) = number::le_u32(i)?; let (i, ntoc) = number::le_u32(i)?; Ok((i, ntoc)) } fn parse_toc(i: &[u8]) -> IResult<&[u8], Toc> { let (i, toctype) = number::le_u32(i)?; // Type let (i, subtype) = number::le_u32(i)?; // Subtype let (i, pos) = number::le_u32(i)?; // Position Ok(( i, Toc { toctype, subtype, pos, }, )) } fn parse_img(i: &[u8]) -> IResult<&[u8], Image> { let (i, _) = bytes::tag([0x24, 0x00, 0x00, 0x00])(i)?; // Header size let (i, _) = bytes::tag([0x02, 0x00, 0xfd, 0xff])(i)?; // Type let (i, size) = number::le_u32(i)?; let (i, _) = bytes::tag([0x01, 0x00, 0x00, 0x00])(i)?; // Image version (1) let (i, width) = number::le_u32(i)?; let (i, height) = number::le_u32(i)?; let (i, xhot) = number::le_u32(i)?; let (i, yhot) = number::le_u32(i)?; let (i, delay) = number::le_u32(i)?; let img_length: usize = (4 * width * height) as usize; let (i, pixels_slice) = bytes::take(img_length)(i)?; let pixels_argb = rgba_to_argb(pixels_slice); let pixels_rgba = Vec::from(pixels_slice); Ok(( i, Image { size, width, height, xhot, yhot, delay, pixels_argb, pixels_rgba, }, )) } /// Converts a RGBA slice into an ARGB vec /// /// Note that, if the input length is not /// a multiple of 4, the extra elements are ignored. fn rgba_to_argb(i: &[u8]) -> Vec { let mut res = Vec::with_capacity(i.len()); for rgba in i.chunks(4) { if rgba.len() < 4 { break; } res.push(rgba[3]); res.push(rgba[0]); res.push(rgba[1]); res.push(rgba[2]); } res } /// Parse an XCursor file into its images. pub fn parse_xcursor(content: &[u8]) -> Option> { let (mut i, ntoc) = parse_header(content).ok()?; let mut imgs = Vec::with_capacity(ntoc as usize); for _ in 0..ntoc { let (j, toc) = parse_toc(i).ok()?; i = j; if toc.toctype == 0xfffd_0002 { let index = toc.pos as usize..; let (_, img) = parse_img(&content[index]).ok()?; imgs.push(img); } } Some(imgs) } #[cfg(test)] mod tests { use super::{parse_header, parse_toc, rgba_to_argb, Toc}; // A sample (and simple) XCursor file generated with xcursorgen. // Contains a single 4x4 image. const FILE_CONTENTS: [u8; 128] = [ 0x58, 0x63, 0x75, 0x72, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x1C, 0x00, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x02, 0x00, 0xFD, 0xFF, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, 0x00, 0x80, ]; #[test] fn test_parse_header() { assert_eq!( parse_header(&FILE_CONTENTS).unwrap(), (&FILE_CONTENTS[16..], 1) ) } #[test] fn test_parse_toc() { let toc = Toc { toctype: 0xfffd0002, subtype: 4, pos: 0x1c, }; assert_eq!( parse_toc(&FILE_CONTENTS[16..]).unwrap(), (&FILE_CONTENTS[28..], toc) ) } #[test] fn test_rgba_to_argb() { let initial: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; assert_eq!(rgba_to_argb(&initial), [3u8, 0, 1, 2, 7, 4, 5, 6]) } #[test] fn test_rgba_to_argb_extra_items() { let initial: [u8; 9] = [0, 1, 2, 3, 4, 5, 6, 7, 8]; assert_eq!(rgba_to_argb(&initial), &[3u8, 0, 1, 2, 7, 4, 5, 6]); } #[test] fn test_rgba_to_argb_no_items() { let initial: &[u8] = &[]; assert_eq!(initial, &rgba_to_argb(initial)[..]); } }