use std::collections::HashSet;
use std::env;
use std::path::{Path, PathBuf};
pub mod parser;
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct CursorTheme {
theme: CursorThemeIml,
search_paths: Vec<PathBuf>,
}
impl CursorTheme {
pub fn load(name: &str) -> Self {
let search_paths = theme_search_paths();
let theme = CursorThemeIml::load(name, &search_paths);
CursorTheme {
theme,
search_paths,
}
}
pub fn load_icon(&self, icon_name: &str) -> Option<PathBuf> {
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 {
name: String,
data: Vec<(PathBuf, Option<String>)>,
}
impl CursorThemeIml {
fn load(name: &str, search_paths: &[PathBuf]) -> Self {
let mut data = Vec::new();
for mut path in search_paths.iter().cloned() {
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,
}
}
fn load_icon(
&self,
icon_name: &str,
search_paths: &[PathBuf],
walked_themes: &mut HashSet<String>,
) -> Option<PathBuf> {
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);
}
}
walked_themes.insert(self.name.clone());
for data in &self.data {
let inherits = match data.1.as_ref() {
Some(inherits) => inherits,
None => continue,
};
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
}
}
fn theme_search_paths() -> Vec<PathBuf> {
let xcursor_path = match env::var("XCURSOR_PATH") {
Ok(xcursor_path) => xcursor_path.split(':').map(PathBuf::from).collect(),
Err(_) => {
let get_icon_dirs = |xdg_path: String| -> Vec<PathBuf> {
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);
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| {
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()
}
fn theme_inherits(file_path: &Path) -> Option<String> {
let content = std::fs::read_to_string(file_path).ok()?;
parse_theme(&content)
}
fn parse_theme(content: &str) -> Option<String> {
const PATTERN: &str = "Inherits";
let is_xcursor_space_or_separator =
|&ch: &char| -> bool { ch.is_whitespace() || ch == ';' || ch == ',' };
for line in content.lines() {
if !line.starts_with(PATTERN) {
continue;
}
let mut chars = line.get(PATTERN.len()..).unwrap().trim_start().chars();
if Some('=') != chars.next() {
continue;
}
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()));
}
}