use std::{env, path::PathBuf}; use tini::Ini; static CURSOR_DIR_NAME: &'static &str = &"cursors"; static CURSOR_INDEX_FILE_NAME: &'static &str = &"index.theme"; static THEME_FILE_CURSOR_SECTION: &'static &str = &"Icon Theme"; static THEME_FILE_INHERITS_KEY: &'static &str = &"Inherits"; static ENV_DATA_DIRS: &'static &str = &"XDG_DATA_DIRS"; static ENV_CURSOR_THEME: &'static &str = &"XCURSOR_THEME"; static DEFAULT_THEME: &'static &str = &"default"; static KNOWN_THEMES: &[&str] = &["Yaru", "Adwaita"]; pub fn ensure_cursor_theme() { // If the XCURSOR_THEME value is explicitly set, // then we do not want to modify the user's environment if env::var(ENV_CURSOR_THEME).is_ok() { return; } let crawler = CursorThemeCrawler::new(); if let Some(theme) = crawler.determine_cursor_theme() { // winit and it's dependencies will automatically check for // the default theme, so we do not need to mess with the // env var here. if theme != *DEFAULT_THEME { env::set_var(ENV_CURSOR_THEME, theme); } } } struct CursorThemeCrawler { /// Directories to search when looking for a cursor theme. /// Directories are searched in vec order from first to last. /// However, because themes can reference other themes, it is /// possible for search results to traverse multiple directories. /// For example, a default theme can be found in directories[1] /// that inherits from a theme in directories[3], which itself /// inherits from a theme in directories[0] directories: Vec, } fn non_empty_var(name: &str) -> Option { env::var(name).ok().filter(|val| !val.is_empty()) } impl CursorThemeCrawler { pub fn new() -> Self { // Per https://specifications.freedesktop.org/icon-theme-spec/icon-theme-spec-latest.html#directory_layout, // we search: // - $HOME/.icons (for backwards compatibility) // - $XDG_DATA_HOME/icons (technically this should be part of XDG_DATA_DIRS, but we add it in here) // - Defaults to $HOME/.local/share // - $XDG_DATA_DIRS/icons // - Defaults to /usr/local/share/:/usr/share/ // - /usr/share/pixmaps let mut directories = vec![]; let xdg_data_dirs = non_empty_var(ENV_DATA_DIRS) .or_else(|| Some("/usr/local/share/:/usr/share/".to_string())); if let Some(home) = dirs::home_dir() { directories.push(home.join(".icons")); } if let Some(xdg_data_home) = dirs::data_dir() { directories.push(xdg_data_home.join("icons")); } if let Some(xdg_data_dirs) = xdg_data_dirs { for dir in xdg_data_dirs.split(':') { if !dir.is_empty() { directories.push(PathBuf::from(dir).join("icons")); } } } directories.push(PathBuf::from("/usr/share/pixmaps")); Self { directories } } /// First checks to see if there is a default cursor theme set. /// If there is no default set, we check a list of known themes. /// The first theme to be confirmed exist is returned, else None /// is returned. fn determine_cursor_theme(&self) -> Option { if self.check_cursor_theme(DEFAULT_THEME) { return Some(DEFAULT_THEME.to_string()); } for theme in KNOWN_THEMES { if self.check_cursor_theme(theme) { return Some((*theme).to_string()); } } None } /// Returns true if an icon theme exists and has a `cursors/` /// folder, indicating that the cursors for that theme are installed. /// Per the specification, an icon theme can exist along multiple /// directories. As long as at least one of those directories /// contains the `cursors/` subdir, we consider it valid fn check_cursor_theme_installed(&self, theme: &str) -> bool { for dir in &self.directories { if dir.join(theme).join(CURSOR_DIR_NAME).exists() { return true; } } false } /// Checks that a given icon theme is a valid cursor theme. /// /// When we check a cursor theme, we verify that either: /// a. The icon theme has a cursors/ folder /// b. The icon theme inherits from an existing cursor theme. /// /// This can cause us to traverse multiple themes as part of our validation. /// we do this verification to handle cases like the `adwaita-icon-theme` /// deb packages, which sets Adwaita to the default icon theme without /// installing a cursor theme. fn check_cursor_theme(&self, root_theme: &str) -> bool { let mut visited = std::collections::HashSet::from([root_theme.to_string()]); let mut pending = std::collections::VecDeque::from([root_theme.to_string()]); while let Some(theme) = pending.pop_front() { if self.check_cursor_theme_installed(&theme) { return true; } // Per the spec, the **first** index.theme found when traversing // the directories is used let inherited_themes = &self .directories .iter() .filter_map(|index_dir| { let index_path = index_dir.join(&theme).join(CURSOR_INDEX_FILE_NAME); if let Ok(theme_file) = Ini::from_file(&index_path) { theme_file.get_vec_with_sep::( THEME_FILE_CURSOR_SECTION, THEME_FILE_INHERITS_KEY, ",", ) } else { None } }) .next(); if let Some(inherited_themes) = inherited_themes { for new_theme in inherited_themes { if !visited.contains(new_theme) { visited.insert(new_theme.clone()); pending.push_back(new_theme.clone()); } } } } false } } #[cfg(test)] #[path = "cursor_theme_tests.rs"] mod tests;