tauri-plugin-vicons 2.1.2

Icon API for Tauri plugins (Created for VasakOS)
Documentation
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::collections::hash_map::Entry;
use std::fs;
use std::sync::{LazyLock, Mutex};
use std::time::{Duration, SystemTime};

use base64::engine::general_purpose::STANDARD;
use base64::Engine;
use gtk::prelude::IconThemeExt;
use tauri::{Emitter, Manager, plugin::PluginApi, AppHandle, Runtime};

use crate::error::Result;
use crate::models::CacheEntry;

static ICON_CACHE: LazyLock<Mutex<HashMap<String, CacheEntry>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));
static SYMBOL_CACHE: LazyLock<Mutex<HashMap<String, CacheEntry>>> =
    LazyLock::new(|| Mutex::new(HashMap::new()));

const CACHE_DURATION: Duration = Duration::from_secs(30 * 60);

fn is_cache_expired(timestamp: SystemTime) -> bool {
    timestamp.elapsed().map_or(true, |d| d > CACHE_DURATION)
}

fn clear_cache_internal() {
    ICON_CACHE.lock().unwrap().clear();
    SYMBOL_CACHE.lock().unwrap().clear();
    crate::logger::info("Icon cache cleared (auto-detected theme change)");
}

pub fn init_theme_monitor<R: Runtime>(app: &AppHandle<R>) {
    if let Some(themed) = gtk::IconTheme::default() {
        let app_clone = app.clone();
        themed.connect_changed(move |_theme| {
            crate::logger::info("Icon theme change detected");
            clear_cache_internal();
            let _ = app_clone.emit("vicons:theme-changed", ());
        });
    } else {
        crate::logger::error("Failed to initialize theme monitor");
    }
}

fn get_cached_icon_data(
    name: &str,
    cache: &Mutex<HashMap<String, CacheEntry>>,
    lookup_flags: gtk::IconLookupFlags,
    icon_type: &str,
) -> Result<String> {
    {
        let mut guard = cache.lock().unwrap();
        match guard.entry(name.to_string()) {
            Entry::Occupied(e) if !is_cache_expired(e.get().timestamp) => {
                return Ok(e.get().data.clone());
            }
            Entry::Occupied(e) => {
                crate::logger::info(&format!("Cache expired for {}: '{}'", icon_type, name));
                e.remove_entry();
            }
            Entry::Vacant(_) => {}
        }
    }

    let themed =
        gtk::IconTheme::default().ok_or(crate::error::Error::ThemeMonitorError)?;

    let mut themed_icon = themed.lookup_icon(name, 64, lookup_flags);

    if themed_icon.is_none() {
        crate::logger::warn(&format!("{} not found: '{}'", icon_type, name));
        themed_icon = themed.lookup_icon("image-missing", 64, lookup_flags);
    }

    let icon = themed_icon
        .ok_or_else(|| crate::error::Error::IconNotFound(name.to_string()))?
        .filename()
        .ok_or_else(|| crate::error::Error::IconNotFound(name.to_string()))?;

    let icon_data = fs::read(icon)?;
    let encoded = STANDARD.encode(icon_data);

    cache
        .lock()
        .unwrap()
        .entry(name.to_string())
        .or_insert(CacheEntry {
            data: encoded.clone(),
            timestamp: SystemTime::now(),
        });

    Ok(encoded)
}

fn read_file_as_base64<P: AsRef<std::path::Path>>(path: P) -> Result<String> {
    let icon_data = fs::read(path.as_ref())?;
    Ok(STANDARD.encode(icon_data))
}

pub fn get_icon_impl(name: &str) -> Result<String> {
    let path = std::path::Path::new(name);
    if path.exists() && path.is_file() {
        crate::logger::info(&format!("Icon from file path: '{}'", name));
        return read_file_as_base64(path);
    }

    get_cached_icon_data(
        name,
        &ICON_CACHE,
        gtk::IconLookupFlags::FORCE_SVG | gtk::IconLookupFlags::FORCE_REGULAR,
        "Icon",
    )
}

pub fn get_symbol_impl(name: &str) -> Result<String> {
    let path = std::path::Path::new(name);
    if path.exists() && path.is_file() {
        crate::logger::info(&format!("Symbol from file path: '{}'", name));
        return read_file_as_base64(path);
    }

    get_cached_icon_data(
        name,
        &SYMBOL_CACHE,
        gtk::IconLookupFlags::FORCE_SYMBOLIC | gtk::IconLookupFlags::FORCE_SVG,
        "Symbol",
    )
}

pub fn init<R: Runtime, C: DeserializeOwned>(
    app: &AppHandle<R>,
    _api: PluginApi<R, C>,
) -> crate::Result<Vicons<R>> {
    let log_dir = app
        .path()
        .app_data_dir()
        .unwrap_or_else(|_| std::path::PathBuf::from("."));
    let log_path = log_dir.join("logs").join("icons.log");
    crate::logger::init(&log_path);

    init_theme_monitor(app);

    Ok(Vicons(app.clone()))
}

pub struct Vicons<R: Runtime>(AppHandle<R>);

impl<R: Runtime> Vicons<R> {}