hyprshell-launcher-lib 4.9.5

A modern GTK4-based window switcher and application launcher for Hyprland
use anyhow::Context;
use core_lib::default::get_all_desktop_files;
use core_lib::util::{ExecType, analyse_exec};
use std::path::Path;
use std::sync::{OnceLock, RwLock, RwLockReadGuard};
use tracing::{debug_span, trace, warn};

#[derive(Debug, Clone)]
pub struct DesktopEntry {
    pub name: Box<str>,
    pub icon: Option<Box<Path>>,
    pub keywords: Vec<Box<str>>,
    pub exec_search: Box<str>,
    pub exec: Box<str>,
    pub exec_path: Option<Box<Path>>,
    /// if launcher text exactly matches this it will be shown (use for flatpak / appimage / ...)
    pub type_search: &'static str,
    pub terminal: bool,
    pub source: Box<Path>,
    pub other: Vec<DesktopAction>,
}

#[derive(Debug, Clone)]
pub struct DesktopAction {
    pub id: Box<str>,
    pub name: Box<str>,
    pub exec: Box<str>,
}

fn get_desktop_file_map() -> &'static RwLock<Vec<DesktopEntry>> {
    static MAP_LOCK: OnceLock<RwLock<Vec<DesktopEntry>>> = OnceLock::new();
    MAP_LOCK.get_or_init(|| RwLock::new(Vec::new()))
}

pub fn get_all_desktop_entries<'a>() -> RwLockReadGuard<'a, Vec<DesktopEntry>> {
    get_desktop_file_map()
        .read()
        .expect("Failed to lock desktop files mutex")
}

pub fn reload_desktop_entries_map() -> anyhow::Result<()> {
    let _span = debug_span!("reload_desktop_entries_map").entered();

    let mut map = get_desktop_file_map()
        .write()
        .map_err(|_| anyhow::anyhow!("Failed to lock desktop file map"))?;
    map.clear();
    for (entry, ini) in get_all_desktop_files()
        .context("unable to get all desktop files")?
        .iter()
    {
        if let Some(section) = ini.get_section("Desktop Entry") {
            let r#type = section.get_first("Type");
            let no_display = section.get_first_as_boolean("NoDisplay");
            if r#type.as_deref() == Some("Application") && no_display.is_none_or(|n| !n) {
                let name = section.get_first("Name");
                let exec = section.get_first("Exec");
                let icon = section.get_first_as_path("Icon");
                let exec_path = section.get_first_as_path("Path");
                let terminal = section.get_first_as_boolean("Terminal").unwrap_or(false);
                let keywords = section.get_all("Keywords").unwrap_or_else(Vec::new);

                if let (Some(name), Some(exec)) = (name, exec) {
                    let mut exec = String::from(exec);
                    for replacement in ["%f", "%F", "%u", "%U"] {
                        exec = exec.replace(replacement, "");
                    }
                    let (exec_search, type_search) = match analyse_exec(&exec) {
                        ExecType::Flatpak(flatpak_identifier, _) => (flatpak_identifier, "flatpak"),
                        ExecType::PWA(_, _) => (Box::from(""), "pwa"),
                        ExecType::FlatpakPWA(flatpak_identifier, _) => {
                            (flatpak_identifier, "flatpak-pwa")
                        }
                        ExecType::AppImage(app_image_identifier, _) => {
                            (app_image_identifier, "appimage")
                        }
                        ExecType::Absolute(exec_name, _) | ExecType::Relative(exec_name) => {
                            (exec_name, "")
                        }
                    };

                    let other = ini
                        .sections()
                        .iter()
                        .filter_map(|(name, section)| {
                            if name.starts_with("Desktop Action ") {
                                let exec = section.get_first("Exec")?;
                                let name_hr = section.get_first("Name")?;
                                Some(DesktopAction {
                                    id: name.clone(),
                                    name: name_hr,
                                    exec,
                                })
                            } else {
                                None
                            }
                        })
                        .collect();

                    map.push(DesktopEntry {
                        name,
                        icon,
                        keywords,
                        exec_search,
                        type_search,
                        exec_path,
                        terminal,
                        exec: exec.into_boxed_str(),
                        source: entry.path().into_boxed_path(),
                        other,
                    });
                }
            }
        } else {
            warn!(
                "Failed to find section 'Desktop Entry' in file: {}",
                entry.path().display()
            );
        }
    }
    drop(map);
    trace!("filled launcher desktop file map");
    Ok(())
}