1use std::collections::HashMap;
12use std::path::PathBuf;
13use std::sync::LazyLock;
14
15use freedesktop_desktop_entry::DesktopEntry;
16use iced::widget::image::Handle as ImageHandle;
17use iced::widget::svg::Handle as SvgHandle;
18use serde::{Deserialize, Serialize};
19
20mod cache;
21
22pub use cache::{clear_cache_dir, Cache};
23
24pub const DEFAULT_ICON_SIZE: u16 = 32;
26
27#[derive(Debug, Clone, PartialEq)]
29pub enum IconHandle {
30 NotLoaded,
31 Raster(ImageHandle),
32 Vector(SvgHandle),
33}
34
35static FALLBACK_ICON_DATA: &[u8] = r##"<?xml version="1.0" encoding="UTF-8" standalone="no"?>
37<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48">
38 <defs>
39 <linearGradient id="b">
40 <stop offset="0" stop-opacity=".32673267"/>
41 <stop offset="1" stop-opacity="0"/>
42 </linearGradient>
43 <linearGradient id="a" x1="99.7773" x2="153.0005" y1="15.4238" y2="248.6311" gradientUnits="userSpaceOnUse">
44 <stop offset="0" stop-color="#184375"/>
45 <stop offset="1" stop-color="#c8bddc"/>
46 </linearGradient>
47 <linearGradient xlink:href="#a" id="d" x1="99.7773" x2="153.0005" y1="15.4238" y2="248.6311" gradientTransform="translate(-.585758 -1.050787) scale(.20069)" gradientUnits="userSpaceOnUse"/>
48 <radialGradient xlink:href="#b" id="c" cx="14.287618" cy="68.872971" r="11.68987" fx="14.287618" fy="72.568001" gradientTransform="matrix(1.39926 0 0 .51326 4.365074 4.839285)" gradientUnits="userSpaceOnUse"/>
49 </defs>
50 <path fill="url(#c)" fill-rule="evenodd" d="M44.285715 38.714287a19.928572 9.837245 0 1 1-39.8571433 0 19.928572 9.837245 0 1 1 39.8571433 0z" color="#000" overflow="visible" style="marker:none" transform="translate(-4.539687 -7.794678) scale(1.18638)"/>
51 <path fill="url(#d)" stroke="#3f4561" stroke-linecap="round" stroke-linejoin="round" d="M24.285801 43.196358 4.3751874 23.285744 24.285801 3.3751291 44.196415 23.285744 24.285801 43.196358h0z"/>
52 <path fill="#fff" d="M43.505062 23.285744 24.285801 4.0664819 5.0665401 23.285744l.7810675.624932L24.45724 5.4825431 43.505256 23.285744h-.000194z" opacity=".72000003"/>
53 <path fill="#fff" d="m8.9257729 27.145172.7384498-1.024184c.6367493.268492 1.3006183.485069 1.9861833.644885l-.005812 1.576858c.427728.088335.86301.156136 1.304105.204371l.481774-1.501889c.344041.028477.691764.044167 1.043361.044167.351209 0 .699124-.015497 1.043166-.044167l.481775 1.501889c.441288-.048235.876376-.116036 1.304104-.204371l-.006005-1.577051c.685758-.159623 1.349433-.3762 1.986182-.644692l.92248 1.279502c.402351-.182094.794241-.382591 1.174895-.600522l-.492817-1.498016c.59723-.36225 1.161723-.773319 1.687471-1.227972l1.272141.931779c.325638-.296581.637329-.608272.933716-.93391l-.931585-1.271947c.454847-.525748.865916-1.090047 1.228166-1.687665l1.498015.493011c.217932-.380848.418623-.772932.600329-1.175088l-1.279308-.922287c.268492-.636749.485068-1.300618.645079-1.986376l1.576663.005811c.088335-.427727.156137-.86301.204178-1.304298l-1.501695-.481774c.028864-.343848.044167-.691764.044167-1.043167 0-.351403-.015691-.699125-.044167-1.043361l1.501695-.481774c-.047847-.441094-.116037-.876183-.203984-1.304104l-1.577051.006005c-.159817-.685759-.376393-1.349627-.644691-1.9861811l1.279308-.9222887c-.181707-.4023513-.382591-.7942415-.600135-1.1750898l-1.498209.4930113c-.362251-.5974244-.773319-1.1617232-1.227973-1.6872772l.931586-1.2721409c-.278372-.3058794-.571078-.5980048-.875408-.8781198L5.0669275 23.285938l1.0069418 1.006942.2987118-.218706c.5257484.454653 1.0900465.865722 1.6874698 1.227972l-.2419526.735157 1.1080622 1.108062-.0003876-.000193zm19.5232031 5.045944c0-6.484682 4.233883-11.979469 10.08724-13.874023l-2.226972-2.227167c-.016854.006975-.0339.01298-.05056.020147l-.181513-.251832-1.412004-1.412004c-.463178.2189-.91667.45446-1.359314.707648l.694089 2.109193c-.841314.509669-1.635748 1.08869-2.375747 1.728732l-1.79111-1.311659c-.458721.41746-.897297.856036-1.314564 1.314565l1.311465 1.790914c-.640041.740195-1.218868 1.534628-1.728731 2.375748l-2.109387-.694089c-.306654.536403-.589093 1.088304-.844994 1.654732l1.801182 1.298293c-.377942.896329-.682852 1.831014-.907758 2.796501l-2.219999-.008524c-.124172.602266-.219869 1.215188-.287476 1.836051l2.114423.678398c-.040293.484293-.061991.97401-.061991 1.46857 0 .494753.021698.98447.061991 1.468763l-2.114423.677816c.067607.621251.163304 1.233979.28767 1.836245l2.219805-.00833c.224906.965487.529816 1.900172.907758 2.796502l-1.801182 1.298486c.142382.31479.293869.624931.452136.930423l3.804023-3.803636c-.61602-1.614245-.95425-3.365836-.95425-5.196269l.000193-.000194z" opacity=".49999997"/>
54 <path d="M5.2050478 23.424252 24.285801 42.505005l19.219261-19.219261-.715099-.682219-18.479649 18.438152L5.2050478 23.424059v.000193z" opacity=".34999999"/>
55</svg>"##
56 .as_bytes();
57
58pub static FALLBACK_ICON_HANDLE: LazyLock<IconHandle> =
60 LazyLock::new(|| IconHandle::Vector(SvgHandle::from_memory(FALLBACK_ICON_DATA)));
61
62fn not_loaded_icon() -> IconHandle {
63 IconHandle::NotLoaded
64}
65
66#[derive(Debug, Serialize, Deserialize, Clone)]
67pub struct AppDescriptor {
69 pub appid: String,
70 pub title: String,
71 #[serde(default)]
72 pub lower_title: String,
73 pub exec: Option<String>,
74 pub exec_count: usize,
75 pub icon_name: Option<String>,
76 #[serde(default)]
77 pub icon_path: Option<PathBuf>,
78 #[serde(skip, default = "not_loaded_icon")]
79 pub icon_handle: IconHandle,
80}
81
82impl From<DesktopEntry> for AppDescriptor {
83 fn from(value: DesktopEntry) -> Self {
84 AppDescriptor {
85 appid: value.appid.clone(),
86 title: value.desktop_entry("Name").expect("get name").to_string(),
87 lower_title: value
88 .desktop_entry("Name")
89 .expect("get name")
90 .to_lowercase(),
91 exec: value.exec().map(str::to_string),
92 exec_count: 0,
93 icon_name: value.icon().map(str::to_string),
94 icon_path: None,
95 icon_handle: IconHandle::NotLoaded,
96 }
97 }
98}
99
100fn preserve_icon_handles(source: &[AppDescriptor], target: &mut [AppDescriptor]) {
102 if source.is_empty() || target.is_empty() {
103 return;
104 }
105
106 let mut source_by_id: HashMap<String, &AppDescriptor> = HashMap::with_capacity(source.len());
107 for app in source {
108 source_by_id.insert(app.appid.clone(), app);
109 }
110
111 for app in target {
112 if let Some(existing) = source_by_id.get(&app.appid) {
113 let same_icon =
114 existing.icon_path == app.icon_path && existing.icon_name == app.icon_name;
115 let handle_loaded = matches!(
116 existing.icon_handle,
117 IconHandle::Raster(_) | IconHandle::Vector(_)
118 );
119 if same_icon && handle_loaded {
120 app.icon_handle = existing.icon_handle.clone();
121 }
122 }
123 }
124}