mirror of
https://github.com/danbulant/icon
synced 2026-05-19 04:08:36 +00:00
Initial commit
This commit is contained in:
commit
15caee2d45
2 changed files with 146 additions and 0 deletions
43
src/icon.rs
Normal file
43
src/icon.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IconFile {
|
||||
pub path: PathBuf,
|
||||
pub file_type: FileType
|
||||
}
|
||||
|
||||
impl IconFile {
|
||||
pub fn from_path(path: &Path) -> Option<IconFile> {
|
||||
let file_type = FileType::from_path_ext(path)?;
|
||||
|
||||
Some(IconFile {
|
||||
path: path.to_owned(),
|
||||
file_type,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub enum FileType {
|
||||
Png,
|
||||
Xmp,
|
||||
Svg,
|
||||
}
|
||||
|
||||
impl FileType {
|
||||
pub fn from_path_ext(path: &Path) -> Option<Self> {
|
||||
let ext = path.extension()?;
|
||||
let ext = ext.to_str()?;
|
||||
|
||||
if ext.eq_ignore_ascii_case("png") {
|
||||
Some(FileType::Png)
|
||||
} else if ext.eq_ignore_ascii_case("xmp") {
|
||||
Some(FileType::Xmp)
|
||||
} else if ext.eq_ignore_ascii_case("svg") {
|
||||
Some(FileType::Svg)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/search_dir.rs
Normal file
103
src/search_dir.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
use std::path::{Path, PathBuf};
|
||||
use crate::icon::IconFile;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SearchDirectories {
|
||||
dirs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
impl SearchDirectories {
|
||||
pub fn default() -> Self {
|
||||
<Self as Default>::default()
|
||||
}
|
||||
|
||||
pub fn search_icons_and_theme_indexes(&self) -> (Vec<IconFile>, Vec<PathBuf>) {
|
||||
fn theme_name_from_path(path: &Path) -> Option<&str> {
|
||||
let theme_name = path.components()
|
||||
.nth_back(1); // get the second-to-last component (which should be the theme name)
|
||||
|
||||
Some(theme_name?.as_os_str().to_str()?)
|
||||
}
|
||||
|
||||
// "Each theme is stored as subdirectories of the base directories"
|
||||
|
||||
let (dirs, files) = self.dirs.iter()
|
||||
.flat_map(|base_dir| base_dir.read_dir()) // read the entries in each base dir
|
||||
.flat_map(std::convert::identity) // merge all the iterators
|
||||
.flat_map(std::convert::identity) // remove Err entries
|
||||
.filter_map(|entry| Some((entry.file_type().ok()?, entry))) // get file type for each entry and skip if fail
|
||||
.partition::<Vec<_>, _>(|(ft, _)| ft.is_dir());
|
||||
|
||||
// icons at the top-level in a base_dir don't belong to a theme, but must still be able to be found!
|
||||
let files = files.into_iter()
|
||||
.flat_map(|(_, entry)| IconFile::from_path(&entry.path()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// "In at least one of the theme directories there must be a file called
|
||||
// index.theme that describes the theme. The first index.theme found while
|
||||
// searching the base directories in order is used"
|
||||
|
||||
let mut indexes = dirs.into_iter()
|
||||
.map(|(_, entry)| entry.path().join("index.theme"))
|
||||
.filter(|path| path.exists()) // the index.theme file must exist
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// only keep the first `index.theme` for each theme
|
||||
indexes.dedup_by_key(|path| theme_name_from_path(&path).map(|s| s.to_string()));
|
||||
|
||||
(files, indexes)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I, P> From<I> for SearchDirectories
|
||||
where
|
||||
I: IntoIterator<Item = P>,
|
||||
P: Into<PathBuf>,
|
||||
{
|
||||
fn from(value: I) -> Self {
|
||||
let dirs = value.into_iter().map(Into::into).collect();
|
||||
|
||||
SearchDirectories { dirs }
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SearchDirectories {
|
||||
fn default() -> Self {
|
||||
// "By default, apps should look in $HOME/.icons (for backwards compatibility),
|
||||
// in $XDG_DATA_DIRS/icons
|
||||
// and in /usr/share/pixmaps (in that order)."
|
||||
|
||||
let xdg = xdg::BaseDirectories::new();
|
||||
|
||||
let mut directories = vec![];
|
||||
|
||||
if let Some(home) = std::env::home_dir() {
|
||||
directories.push(home.join(".icons"));
|
||||
}
|
||||
|
||||
xdg.data_dirs
|
||||
.into_iter()
|
||||
.map(|data_dir| data_dir.join("icons"))
|
||||
.for_each(|dir| directories.push(dir));
|
||||
|
||||
directories.push("/usr/share/pixmaps".into());
|
||||
|
||||
directories.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::search_dir::SearchDirectories;
|
||||
|
||||
// these tests assume certain applications are installed on the system they are ran on.
|
||||
|
||||
#[test]
|
||||
fn test_find_htop_icon() {
|
||||
let dirs = SearchDirectories::default();
|
||||
|
||||
let (icons, _indexes) = dirs.search_icons_and_theme_indexes();
|
||||
|
||||
assert!(icons.iter().any(|i| i.path.file_name().and_then(|s| s.to_str()) == Some("htop.png")))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue