use crate::vault::AppData;
use core::panic;
use std::{
env,
ffi::OsStr,
fs,
io::{BufReader, prelude::*},
path::{Component, Path, PathBuf},
process::Command,
};
const PREFERRED_SIZE: u32 = 48;
const ICON_EXTENSIONS: &[&str] = &["png", "svg", "xpm"];
pub fn get_image_path(val: &str) -> Option<String> {
let theme_name = get_icon_theme_name();
let search_dirs = build_icon_search_dirs();
lookup_icon(val, &theme_name, &search_dirs).map(|p| p.to_string_lossy().into_owned())
}
fn get_icon_theme_name() -> String {
let Ok(out) = Command::new("gsettings")
.args(["get", "org.gnome.desktop.interface", "icon-theme"])
.output()
else {
return "hicolor".to_owned();
};
if !out.status.success() {
return "hicolor".to_owned();
}
let raw = String::from_utf8_lossy(&out.stdout);
let trimmed = raw.trim();
let name = trimmed
.strip_prefix('\'')
.and_then(|s| s.strip_suffix('\''))
.unwrap_or(trimmed);
if name.is_empty() {
"hicolor".to_owned()
} else {
name.to_owned()
}
}
fn build_icon_search_dirs() -> Vec<PathBuf> {
let mut dirs: Vec<PathBuf> = Vec::new();
if let Ok(home) = env::var("HOME") {
dirs.push(PathBuf::from(format!("{}/.icons", home)));
let xdg_data_home =
env::var("XDG_DATA_HOME").unwrap_or_else(|_| format!("{}/.local/share", home));
dirs.push(PathBuf::from(format!("{}/icons", xdg_data_home)));
}
let xdg_data_dirs =
env::var("XDG_DATA_DIRS").unwrap_or_else(|_| "/usr/local/share:/usr/share".to_owned());
for data_dir in xdg_data_dirs.split(':') {
dirs.push(PathBuf::from(format!("{}/icons", data_dir)));
}
dirs
}
fn lookup_icon(icon_name: &str, theme_name: &str, search_dirs: &[PathBuf]) -> Option<PathBuf> {
let mut visited: Vec<String> = Vec::new();
if let Some(path) = lookup_in_theme_chain(icon_name, theme_name, search_dirs, &mut visited) {
return Some(path);
}
if !visited.iter().any(|v| v == "hicolor") {
if let Some(path) = lookup_in_theme_chain(icon_name, "hicolor", search_dirs, &mut visited) {
return Some(path);
}
}
for ext in ICON_EXTENSIONS {
let path = PathBuf::from(format!("/usr/share/pixmaps/{}.{}", icon_name, ext));
if path.exists() {
return Some(path);
}
}
None
}
fn lookup_in_theme_chain(
icon_name: &str,
theme_name: &str,
search_dirs: &[PathBuf],
visited: &mut Vec<String>,
) -> Option<PathBuf> {
if visited.iter().any(|v| v == theme_name) {
return None;
}
visited.push(theme_name.to_owned());
for base_dir in search_dirs {
let theme_path = base_dir.join(theme_name);
if !theme_path.is_dir() {
continue;
}
if let Some(path) = find_icon_in_theme_dir(&theme_path, icon_name) {
return Some(path);
}
}
for parent_theme in parse_theme_inherits(theme_name, search_dirs) {
if let Some(path) = lookup_in_theme_chain(icon_name, &parent_theme, search_dirs, visited) {
return Some(path);
}
}
None
}
fn find_icon_in_theme_dir(theme_path: &Path, icon_name: &str) -> Option<PathBuf> {
let subdirs = read_theme_directories(theme_path);
let mut best: Option<(u32, PathBuf)> = None;
for subdir in &subdirs {
let dist = size_distance(subdir);
let subdir_path = theme_path.join(subdir);
if !subdir_path.is_dir() {
continue;
}
for ext in ICON_EXTENSIONS {
let candidate = subdir_path.join(format!("{}.{}", icon_name, ext));
if candidate.exists() {
let is_better = best.as_ref().map_or(true, |(d, _)| dist < *d);
if is_better {
best = Some((dist, candidate));
}
if dist == 0 {
return best.map(|(_, p)| p);
}
}
}
}
best.map(|(_, p)| p)
}
fn read_theme_directories(theme_path: &Path) -> Vec<String> {
let index_path = theme_path.join("index.theme");
if let Ok(content) = fs::read_to_string(&index_path) {
for line in content.lines() {
if let Some(rest) = line.trim().strip_prefix("Directories=") {
return rest
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();
}
}
}
scan_theme_subdirs_recursive(theme_path, theme_path)
}
fn scan_theme_subdirs_recursive(theme_path: &Path, current: &Path) -> Vec<String> {
let Ok(entries) = current.read_dir() else {
return Vec::new();
};
let mut result = Vec::new();
for entry in entries.flatten() {
if !entry.path().is_dir() {
continue;
}
let relative = entry
.path()
.strip_prefix(theme_path)
.ok()
.map(|p| p.to_string_lossy().into_owned());
if let Some(rel) = relative {
let depth = rel.chars().filter(|&c| c == '/').count();
result.push(rel);
if depth == 0 {
result.extend(scan_theme_subdirs_recursive(theme_path, &entry.path()));
}
}
}
result
}
fn parse_theme_inherits(theme_name: &str, search_dirs: &[PathBuf]) -> Vec<String> {
for base_dir in search_dirs {
let index_path = base_dir.join(theme_name).join("index.theme");
let Ok(content) = fs::read_to_string(&index_path) else {
continue;
};
for line in content.lines() {
if let Some(rest) = line.trim().strip_prefix("Inherits=") {
return rest
.split(',')
.map(|s| s.trim().to_owned())
.filter(|s| !s.is_empty())
.collect();
}
}
}
Vec::new()
}
fn size_distance(subdir: &str) -> u32 {
let base = subdir
.split('/')
.next()
.unwrap_or(subdir)
.split('@')
.next()
.unwrap_or(subdir);
if base.eq_ignore_ascii_case("scalable") {
return PREFERRED_SIZE / 2;
}
if let Some(w_str) = base.split('x').next() {
if let Ok(w) = w_str.parse::<u32>() {
return w.abs_diff(PREFERRED_SIZE);
}
}
u32::MAX
}
fn get_exec_command(input: &str) -> String {
let mut output = String::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.next() {
match c {
'%' => {
if let Some('%') = chars.peek() {
output.push('%');
chars.next();
} else {
chars.next();
}
}
'"' => {
}
_ => {
output.push(c);
}
}
}
if output.ends_with('@') {
output = output.chars().take(output.chars().count() - 7).collect();
}
output.trim().to_owned()
}
fn get_desktop_id(file_path: &Path) -> String {
let mut return_string_val = String::new();
let mut app_index: i32 = -1;
for (index, part) in file_path.components().enumerate() {
if part == Component::Normal(OsStr::new("applications")) {
app_index = index as i32;
}
if index > app_index as usize
&& let Component::Normal(string_val) = part
{
return_string_val.push_str(string_val.to_str().unwrap());
if !part.as_os_str().to_str().unwrap().ends_with(".desktop") {
return_string_val.push('-');
}
}
}
return_string_val
}
pub(super) fn desktop_entry_extracter(file_path: PathBuf) -> Vec<Option<AppData>> {
let desktop_file_id: String = get_desktop_id(&file_path);
let mut image_path: Option<String> = None;
let mut name = String::new();
let mut exec_comm = None;
let file = fs::File::open(&file_path).unwrap();
let buf = BufReader::new(file);
let file_contents: Vec<String> = buf
.lines()
.map(|l| l.expect("Could not parse line"))
.collect();
let mut main_entry_found = false;
let mut main_entry_pushed = false;
let mut return_vector: Vec<Option<AppData>> = Vec::new();
for line in file_contents.iter() {
if line.starts_with('#') || line.is_empty() {
continue;
} else if line.starts_with("[Desktop Entry]") {
main_entry_found = true;
} else if !main_entry_found {
panic!("Main Entry Not found");
} else if line.starts_with('[') {
if !main_entry_pushed {
main_entry_pushed = true;
return_vector.push(Some(AppData {
desktop_file_id: desktop_file_id.clone(),
is_primary: true,
image_path: image_path.clone(),
name: name.clone(),
exec_comm: exec_comm.clone(),
}));
} else {
return_vector.push(Some(AppData {
desktop_file_id: desktop_file_id.clone(),
is_primary: false,
image_path: image_path.clone(),
name: format!("{} - {}", return_vector[0].clone().unwrap().name, name),
exec_comm: exec_comm.clone(),
}));
}
} else {
let (key, val) = line.split_once('=').unwrap(); match key {
"Type" => {
match val {
"Application" => {}
_ => return vec![None],
}
}
"Name" => name = val.to_string(),
"Icon" => {
if val.contains('/') {
image_path = Some(val.to_string());
} else {
image_path = get_image_path(val);
}
}
"Exec" => exec_comm = Some(get_exec_command(val)),
"NoDisplay" => {
if val == "true" {
return vec![None];
}
}
"Hidden" => {
if val == "true" {
return vec![None];
}
}
"Terminal" => {}
_ => {}
}
}
}
if !main_entry_pushed {
return_vector.push(Some(AppData {
desktop_file_id: desktop_file_id.clone(),
is_primary: true,
image_path: image_path.clone(),
name: name.clone(),
exec_comm: exec_comm.clone(),
}));
} else {
return_vector.push(Some(AppData {
desktop_file_id: desktop_file_id.clone(),
is_primary: false,
image_path: image_path.clone(),
name: format!("{} - {}", return_vector[0].clone().unwrap().name, name),
exec_comm: exec_comm.clone(),
}));
}
if name.is_empty() {
panic!("Some necessary enteries are not provided by the entry")
}
return_vector
}