pub mod errors;
mod parser;
pub use errors::{LiniconError, Result};
use file_locker::FileLock;
use memmap2::Mmap;
use parser::{parse_index, Directory, IndexHeader};
use std::{
cmp::{Eq, PartialEq},
collections::{HashMap, VecDeque},
env,
iter::Iterator,
path::{Path, PathBuf},
vec::IntoIter,
};
#[cfg(feature = "system-theme")]
pub use linicon_theme::get_icon_theme as get_system_theme;
const DEFAULT_THEME: &str = "hicolor";
fn search_paths() -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Ok(path) = env::var("HOME") {
if !path.is_empty() {
paths.push(Path::new(&path).join(".icons"));
}
}
if let Ok(data_dirs) = env::var("XDG_DATA_DIRS") {
for path in data_dirs.split(":") {
if !path.is_empty() {
paths.push(Path::new(&path).join("icons"));
}
}
}
paths.push(Path::new("/usr/share/pixmaps").to_owned());
paths
}
fn find_theme_dirs(
dir_name: &str,
search_paths: &[PathBuf],
extra_search_paths: Option<&Vec<PathBuf>>,
) -> Vec<PathBuf> {
if let Some(extra_search_paths) = extra_search_paths {
extra_search_paths
.iter()
.chain(search_paths.iter())
.filter_map(|search_path| {
let path = search_path.join(dir_name);
if path.as_path().exists() {
Some(path)
} else {
None
}
})
.collect()
} else {
search_paths
.iter()
.filter_map(|search_path| {
let path = search_path.join(dir_name);
if path.as_path().exists() {
Some(path)
} else {
None
}
})
.collect()
}
}
fn find_index(theme_dirs: &[PathBuf]) -> Option<PathBuf> {
theme_dirs.iter().find_map(|dir| {
let index_path = dir.join("index.theme");
if index_path.exists() {
Some(index_path)
} else {
None
}
})
}
#[derive(Debug, Eq, PartialEq)]
pub enum IconType {
PNG,
SVG,
XMP,
}
#[derive(Debug, Eq, PartialEq)]
pub struct IconPath {
pub path: PathBuf,
pub theme: String,
pub icon_type: IconType,
pub min_size: u16,
pub max_size: u16,
pub scale: u16,
}
#[derive(Debug)]
pub struct IconIter<'a> {
theme_name: Option<String>,
icon_name: String,
size: Option<u16>,
scale: Option<u16>,
extra_search_paths: Option<Vec<PathBuf>>,
state: Option<IconIterState<'a>>,
failed: bool,
do_fallback: bool,
}
#[derive(Debug)]
struct IconIterState<'a> {
theme_name: String,
sps: Vec<PathBuf>,
theme_dirs: Vec<PathBuf>,
file_lock: FileLock,
mmap: Mmap,
dir_iter: IntoIter<Directory<'a>>,
index_header: IndexHeader,
fallbacks: VecDeque<String>,
}
impl<'a> IconIterState<'a> {
fn init(
theme_name: String,
extra_search_paths: Option<&Vec<PathBuf>>,
) -> Result<Self> {
let sps = search_paths();
let theme_dirs = find_theme_dirs(&theme_name, &sps, extra_search_paths);
if theme_dirs.is_empty() {
return Err(LiniconError::ThemeNotFound(theme_name.clone()));
}
let index_file = find_index(&theme_dirs)
.ok_or(LiniconError::IndexFileNotFound(theme_name.clone()))?;
let file_lock = FileLock::new(&index_file)
.blocking(true)
.lock()
.map_err(|e| LiniconError::OpenIndex {
path: index_file.clone(),
source: e,
})?;
let mmap = unsafe { Mmap::map(&file_lock.file) }.map_err(|e| {
LiniconError::OpenIndex {
path: index_file.clone(),
source: e,
}
})?;
let (index_header, dirs) =
parse_index(unsafe { std::mem::transmute(mmap.as_ref()) })
.map_err(|e| LiniconError::ParseIndex {
path: index_file,
source: e,
})?;
let fallbacks = match &index_header.inherits {
Some(inherits) => {
let mut fallbacks = VecDeque::new();
for theme in inherits.split(",") {
if !theme.is_empty() {
fallbacks.push_back(theme.to_owned());
}
}
fallbacks
}
None => VecDeque::new(),
};
Ok(IconIterState {
theme_name,
sps,
theme_dirs,
file_lock,
mmap,
dir_iter: dirs.into_iter(),
index_header,
fallbacks,
})
}
}
impl<'a> IconIter<'a> {
fn lookup_icon(&mut self) -> Result<Option<IconPath>> {
let state = self.state.as_mut().unwrap();
for dir in &mut state.dir_iter {
if (self.scale.is_none() || self.scale.unwrap() == dir.scale)
&& (self.size.is_none()
|| self.size.unwrap() <= dir.max_size
&& self.size.unwrap() >= dir.min_size)
{
let dir_name = match std::str::from_utf8(dir.name) {
Ok(dir_name) => dir_name,
Err(_e) => {
continue;
}
};
for theme_dir in &mut state.theme_dirs {
let path = theme_dir
.join(dir_name)
.join(format!("{}.svg", self.icon_name));
if path.as_path().exists() {
return Ok(Some(IconPath {
path,
theme: state.theme_name.clone(),
icon_type: IconType::SVG,
min_size: dir.min_size,
max_size: dir.max_size,
scale: dir.scale,
}));
}
let path = theme_dir
.join(dir_name)
.join(format!("{}.png", self.icon_name));
if path.as_path().exists() {
return Ok(Some(IconPath {
path,
theme: state.theme_name.clone(),
icon_type: IconType::PNG,
min_size: dir.min_size,
max_size: dir.max_size,
scale: dir.scale,
}));
}
let path = theme_dir
.join(dir_name)
.join(format!("{}.xpm", self.icon_name));
if path.as_path().exists() {
return Ok(Some(IconPath {
path,
theme: state.theme_name.clone(),
icon_type: IconType::XMP,
min_size: dir.min_size,
max_size: dir.max_size,
scale: dir.scale,
}));
}
}
}
}
if !self.do_fallback {
return Ok(None);
}
if state.theme_name == DEFAULT_THEME {
Ok(None)
} else {
let fallback = match state.fallbacks.pop_front() {
Some(name) => name,
None => DEFAULT_THEME.to_owned(),
};
self.state = match IconIterState::init(
fallback,
self.extra_search_paths.as_ref(),
) {
Ok(state) => Some(state),
Err(e) => {
self.failed = true;
return Err(e);
}
};
self.lookup_icon()
}
}
pub fn from_theme(mut self, theme_name: impl AsRef<str>) -> Self {
if self.state.is_some() {
panic!("linicon: Cannot change icon iterator settings after iteration has started");
}
self.theme_name = Some(theme_name.as_ref().to_owned());
self
}
pub fn with_size(mut self, size: u16) -> Self {
if self.state.is_some() {
panic!("linicon: Cannot change icon iterator settings after iteration has started");
}
self.size = Some(size);
self
}
pub fn with_scale(mut self, scale: u16) -> Self {
if self.state.is_some() {
panic!("linicon: Cannot change icon iterator settings after iteration has started");
}
self.scale = Some(scale);
self
}
pub fn with_search_paths(
mut self,
extra_search_paths: &[impl AsRef<str>],
) -> Result<Self> {
if self.state.is_some() {
panic!("linicon: Cannot change icon iterator settings after iteration has started");
}
self.extra_search_paths =
Some(expand_search_paths(extra_search_paths)?);
Ok(self)
}
pub fn use_fallback_themes(mut self, use_fallback: bool) -> Self {
if self.state.is_some() {
panic!("linicon: Cannot change icon iterator settings after iteration has started");
}
self.do_fallback = use_fallback;
self
}
}
impl<'a> Iterator for IconIter<'a> {
type Item = Result<IconPath>;
fn next(&mut self) -> Option<Self::Item> {
if self.state.is_none() {
self.state = match IconIterState::init(
self.theme_name.take().unwrap_or_else(|| {
#[cfg(feature = "system-theme")]
match linicon_theme::get_icon_theme() {
Some(theme_name) => theme_name,
None => DEFAULT_THEME.to_owned(),
}
#[cfg(not(feature = "system-theme"))]
DEFAULT_THEME.to_owned()
}),
self.extra_search_paths.as_ref(),
) {
Ok(state) => Some(state),
Err(e) => {
self.failed = true;
return Some(Err(e));
}
};
}
if self.failed {
None
} else {
match self.lookup_icon() {
Ok(icon_path) => icon_path.map(|p| Ok(p)),
Err(e) => Some(Err(e)),
}
}
}
}
pub fn lookup_icon<'a>(icon_name: impl AsRef<str>) -> IconIter<'a> {
IconIter {
icon_name: icon_name.as_ref().to_owned(),
theme_name: None,
size: None,
scale: None,
extra_search_paths: None,
state: None,
failed: false,
do_fallback: true,
}
}
#[cfg(feature = "expand-paths")]
fn expand_search_paths(
search_paths: &[impl AsRef<str>],
) -> Result<Vec<PathBuf>> {
search_paths
.iter()
.map(|path| {
shellexpand::full(path)
.map(|expanded| Path::new(expanded.as_ref()).to_owned())
.map_err(LiniconError::from)
})
.collect::<Result<_>>()
}
#[cfg(not(feature = "expand-paths"))]
fn expand_search_paths(
search_paths: &[impl AsRef<str>],
) -> Result<Vec<PathBuf>> {
Ok(search_paths
.iter()
.map(|expanded| Path::new(expanded.as_ref()).to_owned())
.collect())
}
#[derive(Debug, PartialEq, Eq)]
pub struct Theme {
pub name: String,
pub display_name: String,
pub paths: Vec<PathBuf>,
pub inherits: Option<Vec<String>>,
pub comment: Option<String>,
}
pub fn themes() -> Vec<Theme> {
_themes(None)
}
pub fn themes_with_extra_paths(
extra_search_paths: &[impl AsRef<str>],
) -> Result<Vec<Theme>> {
Ok(_themes(Some(&expand_search_paths(extra_search_paths)?)))
}
fn _themes(extra_search_paths: Option<&Vec<PathBuf>>) -> Vec<Theme> {
let search_paths = search_paths();
let mut themes: HashMap<String, Vec<PathBuf>> = HashMap::new();
for path in &search_paths {
for entry in match std::fs::read_dir(&path) {
Ok(iter) => iter,
Err(_) => continue,
} {
match entry {
Ok(entry) => match entry.file_name().into_string() {
Ok(s) => {
themes
.entry(s)
.and_modify(|e| e.push(entry.path()))
.or_insert(vec![entry.path()]);
}
Err(_) => continue,
},
Err(_) => continue,
}
}
}
themes
.into_iter()
.filter_map(|(name, paths)| {
let theme_dirs =
find_theme_dirs(&name, &search_paths, extra_search_paths);
match find_index(&theme_dirs) {
Some(index_file) => {
let file_lock = match FileLock::new(&index_file)
.writeable(false)
.blocking(true)
.lock()
{
Ok(f) => f,
Err(_) => return None,
};
let mmap = match unsafe { Mmap::map(&file_lock.file) } {
Ok(v) => v,
Err(_) => return None,
};
let (index_header, _) = match parse_index(mmap.as_ref()) {
Ok(v) => v,
Err(_) => return None,
};
Some(Theme {
name: name.to_owned(),
paths,
display_name: index_header.name,
inherits: index_header.inherits.map(|s| {
s.split(",").map(|s| s.to_owned()).collect()
}),
comment: index_header.comment,
})
}
None => None,
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn do_lookup() {
let wireshark_icons: Vec<_> = lookup_icon("wireshark")
.from_theme("Faenza")
.with_size(64)
.with_scale(1)
.collect();
assert_eq!(
wireshark_icons
.into_iter()
.filter(Result::is_ok)
.map(Result::unwrap)
.filter(|ic| ic.path.display().to_string().contains("Faenza"))
.count(),
2
);
}
#[test]
fn do_lookup_no_size() {
let wireshark_icons: Vec<_> = lookup_icon("wireshark")
.from_theme("Faenza")
.with_scale(1)
.collect();
assert_eq!(
wireshark_icons
.into_iter()
.filter(Result::is_ok)
.map(Result::unwrap)
.filter(|ic| ic.path.display().to_string().contains("Faenza"))
.count(),
8
);
}
#[test]
fn do_lookup_no_scale() {
let wireshark_icons: Vec<_> = lookup_icon("wireshark")
.from_theme("Faenza")
.with_size(64)
.collect();
assert_eq!(
wireshark_icons
.into_iter()
.filter(Result::is_ok)
.map(Result::unwrap)
.filter(|ic| ic.path.display().to_string().contains("Faenza"))
.count(),
2
);
}
#[test]
fn do_lookup_no_size_scale() {
let wireshark_icons: Vec<_> =
lookup_icon("wireshark").from_theme("Faenza").collect();
assert_eq!(
wireshark_icons
.into_iter()
.filter(Result::is_ok)
.map(Result::unwrap)
.filter(|ic| ic.path.display().to_string().contains("Faenza"))
.count(),
8
);
}
#[test]
fn move_threads() {
let mut iter = lookup_icon("wireshark")
.from_theme("Faenza")
.with_size(64)
.with_scale(1);
let first = iter.next();
assert_eq!(
first.unwrap().unwrap().path.display().to_string(),
"/usr/share/icons/Faenza/apps/64/wireshark.png"
);
assert!(std::thread::spawn(move || {
let second = iter.next();
second.unwrap().unwrap().path.display().to_string()
== "/usr/share/icons/Faenza/apps/scalable/wireshark.svg"
})
.join()
.unwrap());
}
#[test]
fn move_iter() {
let mut iter1 = lookup_icon("wireshark")
.from_theme("Faenza")
.with_size(64)
.with_scale(1);
let mut iter2 = lookup_icon("wireshark")
.from_theme("Faenza")
.with_size(64)
.with_scale(1);
std::mem::swap(&mut iter1, &mut iter2);
std::mem::drop(iter1);
assert_eq!(
iter2
.into_iter()
.filter(Result::is_ok)
.map(Result::unwrap)
.filter(|ic| ic.path.display().to_string().contains("Faenza"))
.count(),
2
);
}
#[test]
fn get_themes() {
let theme = themes()
.into_iter()
.find(|theme| theme.name == "Faenza")
.unwrap();
assert_eq!(
theme,
Theme {
name: "Faenza".to_owned(),
display_name: "Faenza".to_owned(),
paths: vec![Path::new("/usr/share/icons/Faenza").to_owned()],
inherits: Some(vec!["gnome".to_owned(), "hicolor".to_owned()]),
comment: Some(
"Icon theme project with tilish style, by Tiheum"
.to_owned()
),
}
);
}
#[cfg(feature = "expand-paths")]
mod expand_paths {
use super::*;
#[test]
fn lookup_and_expand() {
let other_paths = ["~/.local/share"];
let wireshark_icons: Vec<_> = lookup_icon("wireshark")
.from_theme("Faenza")
.with_size(64)
.with_scale(1)
.with_search_paths(&other_paths)
.unwrap()
.collect();
assert_eq!(
wireshark_icons
.into_iter()
.filter(Result::is_ok)
.map(Result::unwrap)
.filter(|ic| ic
.path
.display()
.to_string()
.contains("Faenza"))
.count(),
2
);
}
}
}