use std::collections::HashSet;
use std::env;
use std::path::{Path, PathBuf};
pub mod parser;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum CursorFormat {
XCursor,
Scalable,
}
impl CursorFormat {
fn subdir(self) -> &'static str {
match self {
CursorFormat::XCursor => "cursors",
CursorFormat::Scalable => "cursors_scalable",
}
}
fn matches(self, path: &Path) -> bool {
match self {
CursorFormat::XCursor => path.is_file(),
CursorFormat::Scalable => path.is_dir(),
}
}
}
#[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(SearchPathsEnvironment::get());
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_with_depth(
icon_name,
CursorFormat::XCursor,
&self.search_paths,
&mut walked_themes,
)
.map(|(pathbuf, _)| pathbuf)
}
pub fn load_icon_with_depth(&self, icon_name: &str) -> Option<(PathBuf, usize)> {
let mut walked_themes = HashSet::new();
self.theme.load_icon_with_depth(
icon_name,
CursorFormat::XCursor,
&self.search_paths,
&mut walked_themes,
)
}
pub fn load_scalable(&self, icon_name: &str) -> Option<PathBuf> {
let mut walked_themes = HashSet::new();
self.theme
.load_icon_with_depth(
icon_name,
CursorFormat::Scalable,
&self.search_paths,
&mut walked_themes,
)
.map(|(pathbuf, _)| pathbuf)
}
pub fn load_scalable_with_depth(&self, icon_name: &str) -> Option<(PathBuf, usize)> {
let mut walked_themes = HashSet::new();
self.theme.load_icon_with_depth(
icon_name,
CursorFormat::Scalable,
&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_with_depth(
&self,
icon_name: &str,
format: CursorFormat,
search_paths: &[PathBuf],
walked_themes: &mut HashSet<String>,
) -> Option<(PathBuf, usize)> {
for data in &self.data {
let mut icon_path = data.0.clone();
icon_path.push(format.subdir());
icon_path.push(icon_name);
if format.matches(&icon_path) {
return Some((icon_path, 0));
}
}
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_with_depth(
icon_name,
format,
search_paths,
walked_themes,
) {
Some((icon_path, depth)) => return Some((icon_path, depth + 1)),
None => continue,
}
}
None
}
}
#[derive(Default)]
struct SearchPathsEnvironment {
home: Option<String>,
xcursor_path: Option<String>,
xdg_data_home: Option<String>,
xdg_data_dirs: Option<String>,
}
impl SearchPathsEnvironment {
fn get() -> Self {
SearchPathsEnvironment {
home: env::var("HOME").ok().filter(|x| !x.is_empty()),
xcursor_path: env::var("XCURSOR_PATH").ok().filter(|x| !x.is_empty()),
xdg_data_home: env::var("XDG_DATA_HOME").ok().filter(|x| !x.is_empty()),
xdg_data_dirs: env::var("XDG_DATA_DIRS").ok().filter(|x| !x.is_empty()),
}
}
}
fn theme_search_paths(environment: SearchPathsEnvironment) -> Vec<PathBuf> {
let home_dir = environment
.home
.as_ref()
.map(|home| Path::new(home.as_str()));
if let Some(xcursor_path) = environment.xcursor_path {
return xcursor_path
.split(':')
.flat_map(|entry| {
if entry.is_empty() {
return None;
}
expand_home_dir(PathBuf::from(entry), home_dir)
})
.collect();
}
let mut paths = Vec::new();
if let Some(xdg_data_home) = environment.xdg_data_home {
paths.extend(expand_home_dir(PathBuf::from(xdg_data_home), home_dir));
} else if let Some(home_dir) = home_dir {
paths.push(home_dir.join(".local/share/icons"))
}
if let Some(home_dir) = home_dir {
paths.push(home_dir.join(".icons"));
}
if let Some(xdg_data_dirs) = environment.xdg_data_dirs {
paths.extend(xdg_data_dirs.split(':').flat_map(|entry| {
if entry.is_empty() {
return None;
}
let mut entry = expand_home_dir(PathBuf::from(entry), home_dir)?;
entry.push("icons");
Some(entry)
}))
} else {
paths.push(PathBuf::from("/usr/local/share/icons"));
paths.push(PathBuf::from("/usr/share/icons"));
}
paths.push(PathBuf::from("/usr/share/pixmaps"));
if let Some(home_dir) = home_dir {
paths.push(home_dir.join(".cursors"));
}
paths.push(PathBuf::from("/usr/share/cursors/xorg-x11"));
paths
}
fn expand_home_dir(path: PathBuf, home_dir: Option<&Path>) -> Option<PathBuf> {
let mut components = path.iter();
if let Some(first_component) = components.next() {
if first_component == "~" {
if let Some(home_dir) = home_dir {
let mut path = home_dir.to_path_buf();
for component in components {
path.push(component);
}
return Some(path);
} else {
return None;
}
}
}
Some(path)
}
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::*;
use std::path::{Path, PathBuf};
#[test]
fn test_parse_theme() {
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()));
}
#[test]
fn test_expand_home_dir() {
let home = Path::new("/home/user");
let result = expand_home_dir("~".into(), Some(home));
assert_eq!(result, Some("/home/user".into()));
let result = expand_home_dir("~/.icons".into(), Some(home));
assert_eq!(result, Some("/home/user/.icons".into()));
let result = expand_home_dir("~/.local/share/icons".into(), Some(home));
assert_eq!(result, Some("/home/user/.local/share/icons".into()));
let result = expand_home_dir("~/.icons".into(), None);
assert_eq!(result, None);
let path: PathBuf = "/usr/share/icons".into();
let result = expand_home_dir(path.clone(), Some(home));
assert_eq!(result, Some(path));
let path: PathBuf = "".into();
let result = expand_home_dir(path.clone(), Some(home));
assert_eq!(result, Some(path));
let path: PathBuf = "/some/path/~/icons".into();
let result = expand_home_dir(path.clone(), Some(home));
assert_eq!(result, Some(path));
}
#[test]
fn test_theme_search_paths() {
assert_eq!(
theme_search_paths(SearchPathsEnvironment {
home: Some("/home/user".to_string()),
xdg_data_home: Some("/home/user/.data".to_string()),
xdg_data_dirs: Some("/opt/share::/usr/local/share:~/custom/share".to_string()),
..Default::default()
}),
vec![
PathBuf::from("/home/user/.data"),
PathBuf::from("/home/user/.icons"),
PathBuf::from("/opt/share/icons"),
PathBuf::from("/usr/local/share/icons"),
PathBuf::from("/home/user/custom/share/icons"),
PathBuf::from("/usr/share/pixmaps"),
PathBuf::from("/home/user/.cursors"),
PathBuf::from("/usr/share/cursors/xorg-x11"),
]
);
assert_eq!(
theme_search_paths(SearchPathsEnvironment {
home: Some("/home/user".to_string()),
xcursor_path: Some("~/custom/xcursor/icons:/absolute-path/icons".to_string()),
..Default::default()
}),
vec![
PathBuf::from("/home/user/custom/xcursor/icons"),
PathBuf::from("/absolute-path/icons")
]
);
assert_eq!(
theme_search_paths(SearchPathsEnvironment {
xdg_data_home: Some("~/.data".to_string()),
..Default::default()
}),
vec![
PathBuf::from("/usr/local/share/icons"),
PathBuf::from("/usr/share/icons"),
PathBuf::from("/usr/share/pixmaps"),
PathBuf::from("/usr/share/cursors/xorg-x11"),
]
);
}
}