use crate::IconHandle;
use quick_xml::Reader;
use quick_xml::events::Event;
use std::fs;
use std::path::{Path, PathBuf};
const TARGET_PX: u32 = 64;
pub(super) fn get_icon(exe_path: &str) -> Option<IconHandle> {
let manifest_dir = find_manifest_dir(exe_path)?;
let icon_path = resolve_from_manifest(&manifest_dir)?;
Some(IconHandle::Image(iced::widget::image::Handle::from_path(
icon_path,
)))
}
fn find_manifest_dir(exe_path: &str) -> Option<PathBuf> {
let exe_path = Path::new(exe_path);
let mut dir = exe_path.parent()?;
loop {
if dir.join("AppxManifest.xml").exists() {
return Some(dir.to_path_buf());
}
dir = dir.parent()?;
}
}
fn resolve_from_manifest(manifest_dir: &Path) -> Option<PathBuf> {
let manifest_path = manifest_dir.join("AppxManifest.xml");
let manifest = fs::read_to_string(&manifest_path).ok()?;
let candidates = extract_logo_candidates(&manifest);
for (logo_rel, base_size) in &candidates {
let full = manifest_dir.join(logo_rel);
if let Some(path) = find_best_variant(&full, *base_size) {
return Some(path);
}
if full.is_file() {
return Some(full);
}
}
None
}
fn extract_logo_candidates(manifest: &str) -> Vec<(String, Option<u32>)> {
let mut visual_elements = Vec::new();
let mut package_logo: Option<String> = None;
let mut reader = Reader::from_str(manifest);
loop {
match reader.read_event() {
Ok(Event::Empty(ref e) | Event::Start(ref e))
if e.local_name().as_ref() == b"VisualElements" =>
{
for (attr, base) in [
(b"Square44x44Logo".as_slice(), 44u32),
(b"Square150x150Logo".as_slice(), 150),
] {
if let Some(value) = get_attr(e, attr) {
visual_elements.push((value, Some(base)));
}
}
}
Ok(Event::Start(ref e)) if e.local_name().as_ref() == b"Logo" => {
if let Ok(logo) = reader
.read_text(e.to_end().name())
.map(|t| t.trim().to_string())
&& !logo.is_empty()
{
package_logo = Some(logo);
}
}
Ok(Event::Eof) | Err(_) => break,
_ => {}
}
}
if let Some(logo) = package_logo {
visual_elements.push((logo, None));
}
visual_elements
}
fn get_attr(e: &quick_xml::events::BytesStart, name: &[u8]) -> Option<String> {
for attr in e.attributes().filter_map(Result::ok) {
if attr.key.as_ref() == name {
return String::from_utf8(attr.value.to_vec()).ok();
}
}
None
}
fn find_best_variant(icon_path: &Path, base_size: Option<u32>) -> Option<PathBuf> {
let parent = icon_path.parent()?;
let stem = icon_path.file_stem()?.to_str()?;
let ext = icon_path.extension()?.to_str()?;
let suffix = format!(".{ext}");
let entries = fs::read_dir(parent).ok()?;
let exclude = ["contrast-", "theme-"];
let mut variants: Vec<(PathBuf, Option<u32>)> = Vec::new();
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if !path.is_file() {
continue;
}
let name = match path.file_name().and_then(|n| n.to_str()) {
Some(n) => n.to_string(),
None => continue,
};
if !name.starts_with(stem) || !name.ends_with(&suffix) {
continue;
}
if exclude.iter().any(|e| name.to_lowercase().contains(e)) {
continue;
}
let pixel_size = extract_targetsize(&name).or_else(|| {
let scale = extract_scale(&name)?;
Some(base_size? * scale / 100)
});
variants.push((path, pixel_size));
}
variants
.into_iter()
.min_by_key(|(_, px)| px.map(|s| s.abs_diff(TARGET_PX)).unwrap_or(u32::MAX))
.map(|(path, _)| path)
}
fn extract_targetsize(name: &str) -> Option<u32> {
let after = name.split("targetsize-").nth(1)?;
let num: String = after.chars().take_while(char::is_ascii_digit).collect();
num.parse().ok()
}
fn extract_scale(name: &str) -> Option<u32> {
let after = name.split("scale-").nth(1)?;
let num: String = after.chars().take_while(char::is_ascii_digit).collect();
num.parse().ok()
}