worf/src/desktop/mod.rs
2025-04-06 22:34:04 +02:00

159 lines
4.9 KiB
Rust

use gtk4::prelude::*;
use gtk4::{IconLookupFlags, IconTheme, TextDirection};
use home::home_dir;
use ini::configparser::ini::Ini;
use log::{info, warn};
use regex::Regex;
use std::collections::HashMap;
use std::path::Path;
use std::path::PathBuf;
use std::{fs, string};
pub struct IconResolver {
cache: HashMap<String, String>,
}
impl IconResolver {
#![allow(clippy::single_call_fn)]
pub fn new() -> IconResolver {
IconResolver {
cache: HashMap::new(),
}
}
pub fn icon_path(&mut self, icon_name: &str) -> String {
if let Some(icon_path) = self.cache.get(icon_name) {
info!("Fetching {icon_name} from cache");
return icon_path.to_owned();
}
info!("Loading icon for {icon_name}");
let icon = fetch_icon_from_theme(icon_name)
.or_else(|| fetch_icon_from_common_dirs(icon_name))
.or_else(|| fetch_icon_from_desktop_file(icon_name))
.unwrap_or_else(|| {
warn!("Missing icon for {icon_name}, using fallback");
default_icon()
});
self.cache.insert(icon_name.to_owned(), icon.clone());
self.cache.get(icon_name).unwrap().to_owned()
}
}
pub fn default_icon() -> String {
fetch_icon_from_theme("image-missing").unwrap()
}
fn fetch_icon_from_desktop_file(icon_name: &str) -> Option<String> {
find_desktop_files().into_iter()
.find_map(|desktop_file| {
desktop_file
.get("Desktop Entry")
.filter(|desktop_entry| {
desktop_entry
.get("Exec")
.and_then(|opt| opt.as_ref())
.is_some_and(|exec| exec.to_lowercase().contains(icon_name))
})
.map(|desktop_entry| {
desktop_entry
.get("Icon")
.and_then(|opt| opt.as_ref())
.map(ToOwned::to_owned)
.unwrap_or_default()
})
})
}
fn fetch_icon_from_theme(icon_name: &str) -> Option<String> {
let display = gtk4::gdk::Display::default();
if display.is_none() {
log::error!("Failed to get display");
}
let display = display.unwrap();
let theme = IconTheme::for_display(&display);
let icon = theme.lookup_icon(
icon_name,
&[],
32,
1,
TextDirection::None,
IconLookupFlags::empty(),
);
icon.file()
.and_then(|file| file.path())
.and_then(|path| path.to_str().map(string::ToString::to_string))
}
fn fetch_icon_from_common_dirs(icon_name: &str) -> Option<String> {
let mut paths = vec![
PathBuf::from("/usr/local/share/icons"),
PathBuf::from("/usr/share/icons"),
PathBuf::from("/usr/share/pixmaps"),
// /usr/share/icons contains the theme icons, handled via separate function
];
if let Some(home) = home_dir() {
paths.push(home.join(".local/share/icons"));
}
let extensions = ["png", "jpg", "gif", "svg"].join("|"); // Create regex group for extensions
let formatted_name = Regex::new(&format!(r"(?i){icon_name}\.({extensions})$")).unwrap();
paths
.into_iter()
.filter(|dir| dir.exists())
.find_map(|dir| {
find_file_case_insensitive(dir.as_path(), &formatted_name)
.and_then(|files| files.first().map(|f| f.to_string_lossy().into_owned()))
})
}
fn find_file_case_insensitive(folder: &Path, file_name: &Regex) -> Option<Vec<PathBuf>> {
if !folder.exists() || !folder.is_dir() {
return None;
}
fs::read_dir(folder).ok().map(|entries| {
entries
.filter_map(Result::ok)
.filter(|entry| {
entry
.file_type()
.ok()
.is_some_and(|file_type| file_type.is_file())
})
.filter(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| file_name.is_match(name))
})
.map(|entry| entry.path())
.collect()
})
}
pub(crate) fn find_desktop_files() -> Vec<HashMap<String, HashMap<String, Option<String>>>> {
let mut paths = vec![PathBuf::from("/usr/share/applications")];
if let Some(home) = home_dir() {
paths.push(home.join(".local/share/applications"));
}
let p: Vec<_> = paths
.into_iter()
.filter(|icon_dir| icon_dir.exists())
.filter_map(|icon_dir| {
find_file_case_insensitive(&icon_dir, &Regex::new("(?i).*\\.desktop$").unwrap())
})
.flat_map(|desktop_files| {
desktop_files.into_iter().filter_map(|desktop_file| {
let mut conf = Ini::new();
conf.load(desktop_file.as_path().to_str().unwrap()).ok()
})
}).collect();
p
}