From d90d71a32d20777556573415b85df28bd3ffc0ef Mon Sep 17 00:00:00 2001 From: galister <22305755+galister@users.noreply.github.com> Date: Sat, 27 Dec 2025 18:48:33 +0900 Subject: [PATCH] glob-based icon discovery but very slow --- Cargo.lock | 11 +- dash-frontend/Cargo.toml | 5 +- dash-frontend/src/frontend.rs | 8 +- dash-frontend/src/tab/apps.rs | 7 +- dash-frontend/src/util/desktop_finder.rs | 233 +++++++++++++---------- dash-frontend/src/util/various.rs | 4 +- dash-frontend/src/views/app_launcher.rs | 28 ++- dash-frontend/src/views/process_list.rs | 12 +- 8 files changed, 168 insertions(+), 140 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0dd7958..5435231 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1429,6 +1429,7 @@ dependencies = [ "chrono", "freedesktop", "glam", + "glob", "http-body-util", "hyper", "keyvalues-parser", @@ -1998,7 +1999,6 @@ source = "git+https://github.com/galister/freedesktop.git?rev=2c1e653#2c1e653afc dependencies = [ "freedesktop-apps", "freedesktop-core", - "freedesktop-icon", ] [[package]] @@ -2019,15 +2019,6 @@ dependencies = [ "dirs", ] -[[package]] -name = "freedesktop-icon" -version = "0.0.3" -source = "git+https://github.com/galister/freedesktop.git?rev=2c1e653#2c1e653afcd025c3254a25dcdd5e8750e263eebf" -dependencies = [ - "freedesktop-core", - "rust-ini", -] - [[package]] name = "futures" version = "0.3.31" diff --git a/dash-frontend/Cargo.toml b/dash-frontend/Cargo.toml index 66e991c..8f2e389 100644 --- a/dash-frontend/Cargo.toml +++ b/dash-frontend/Cargo.toml @@ -9,11 +9,11 @@ wgui = { path = "../wgui/" } wlx-common = { path = "../wlx-common" } anyhow.workspace = true -freedesktop = { workspace = true, features = ["apps", "icon"] } +freedesktop = { workspace = true, features = ["apps"] } glam = { workspace = true, features = ["mint", "serde"] } log.workspace = true rust-embed.workspace = true -serde.workspace = true +serde = { workspace = true, features = ["rc"] } serde_json.workspace = true chrono = "0.4.42" @@ -23,3 +23,4 @@ hyper = { version = "1.8.1", features = ["client", "http1", "http2"] } http-body-util = "0.1.3" async-native-tls = "0.5.0" smol-hyper = "0.1.1" +glob = "0.3.3" diff --git a/dash-frontend/src/frontend.rs b/dash-frontend/src/frontend.rs index befd7cb..5ee430f 100644 --- a/dash-frontend/src/frontend.rs +++ b/dash-frontend/src/frontend.rs @@ -19,10 +19,11 @@ use wlx_common::{dash_interface::BoxDashInterface, timestep::Timestep}; use crate::{ assets, settings, tab::{ - Tab, TabType, apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, - settings::TabSettings, + apps::TabApps, games::TabGames, home::TabHome, monado::TabMonado, processes::TabProcesses, settings::TabSettings, + Tab, TabType, }, util::{ + desktop_finder::DesktopFinder, popup_manager::{MountPopupParams, PopupManager, PopupManagerParams}, toast_manager::ToastManager, various::AsyncExecutor, @@ -63,6 +64,8 @@ pub struct Frontend { window_audio_settings: WguiWindow, view_audio_settings: Option, + + pub(crate) desktop_finder: DesktopFinder, } pub struct InitParams { @@ -146,6 +149,7 @@ impl Frontend { window_audio_settings: WguiWindow::default(), view_audio_settings: None, executor: Rc::new(smol::LocalExecutor::new()), + desktop_finder: DesktopFinder::new(), }; // init some things first diff --git a/dash-frontend/src/tab/apps.rs b/dash-frontend/src/tab/apps.rs index e45ce45..2e8f246 100644 --- a/dash-frontend/src/tab/apps.rs +++ b/dash-frontend/src/tab/apps.rs @@ -14,7 +14,6 @@ use crate::{ frontend::{Frontend, FrontendTask, FrontendTasks}, tab::{Tab, TabType}, util::{ - self, desktop_finder::DesktopEntry, popup_manager::{MountPopupParams, PopupHandle}, }, @@ -117,7 +116,7 @@ impl TabApps { extra: Default::default(), }; - let entries = util::desktop_finder::find_entries()?; + let entries = frontend.desktop_finder.find_entries()?; let frontend_tasks = frontend.tasks.clone(); let globals = frontend.layout.state.globals.clone(); @@ -173,7 +172,7 @@ impl AppList { entry .icon_path .as_ref() - .map_or_else(|| Rc::from(""), |icon_path| Rc::from(icon_path.as_str())), + .map_or_else(|| Rc::from(""), |icon_path| icon_path.clone()), ); // entry fallback (question mark) icon @@ -186,7 +185,7 @@ impl AppList { }, ); - template_params.insert(Rc::from("name"), Rc::from(entry.app_name.as_str())); + template_params.insert(Rc::from("name"), entry.app_name.clone()); let data = parser_state.parse_template( doc_params, diff --git a/dash-frontend/src/util/desktop_finder.rs b/dash-frontend/src/util/desktop_finder.rs index 9189faf..e8cef60 100644 --- a/dash-frontend/src/util/desktop_finder.rs +++ b/dash-frontend/src/util/desktop_finder.rs @@ -1,122 +1,159 @@ -use freedesktop::{ApplicationEntry, IconTheme}; +use std::{collections::HashMap, ffi::OsStr, rc::Rc}; + +use freedesktop::{xdg_data_dirs, xdg_data_home, ApplicationEntry}; use serde::{Deserialize, Serialize}; -// compatibility with wayvr-ipc -// TODO: remove this after we're done with the old wayvr-dashboard and use DesktopEntry instead -#[derive(Debug, Deserialize, Serialize)] -pub struct DesktopFile { - pub name: String, - pub icon: Option, - pub exec_path: String, - pub exec_args: Vec, - pub categories: Vec, -} - -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct DesktopEntry { - pub exec_path: String, - pub exec_args: Vec, - pub app_name: String, - pub icon_path: Option, - pub categories: Vec, + pub exec_path: Rc, + pub exec_args: Rc, + pub app_name: Rc, + pub icon_path: Option>, } -#[allow(dead_code)] // TODO: remove this -pub struct EntrySearchCell { - pub exec_path: String, - pub exec_args: Vec, - pub app_name: String, - pub icon_name: Option, - pub categories: Vec, -} - -const ICON_SIZE: u32 = 128; - const CMD_BLACKLIST: [&str; 1] = [ "lsp-plugins", // LSP Plugins collection. They clutter the application list a lot ]; const CATEGORY_TYPE_BLACKLIST: [&str; 5] = ["GTK", "Qt", "X-XFCE", "X-Bluetooth", "ConsoleOnly"]; -pub fn find_entries() -> anyhow::Result> { - let mut res = Vec::::new(); - let theme = IconTheme::current(); +pub struct DesktopFinder { + size_preferences: Vec<&'static OsStr>, + icon_cache: HashMap>, + icon_folders: Vec, +} - 'outer: for app_entry in ApplicationEntry::all() { - let Some(app_entry_id) = app_entry.id() else { - log::warn!( - "No desktop entry id for application \"{}\"", - app_entry.name().as_deref().unwrap_or("") - ); - continue; - }; +impl DesktopFinder { + pub fn new() -> Self { + let data_home = xdg_data_home(); - let Some(name) = app_entry.name() else { - log::warn!("No Name on desktop entry {}", app_entry_id); - continue; - }; + let mut icon_folders = vec![ + // XDG_DATA_HOME takes priority + { + let mut data_home_flatpak = data_home.clone(); + data_home_flatpak.push("flatpak/exports/share/icons"); + data_home_flatpak.to_string_lossy().to_string() + }, + { + let mut data_home = data_home.clone(); + data_home.push("icons"); + data_home.to_string_lossy().to_string() + }, + "/var/lib/flatpak/exports/share/icons".into(), + ]; - let Some(exec) = app_entry.exec() else { - log::warn!("No Exec on desktop entry {}", app_entry_id); - continue; - }; - - if app_entry.no_display() || app_entry.is_hidden() || app_entry.terminal() { - continue; + let data_dirs = xdg_data_dirs(); + for mut data_dir in data_dirs { + data_dir.push("icons"); + icon_folders.push(data_dir.to_string_lossy().to_string()); } - for blacklisted in CMD_BLACKLIST { - if exec.contains(blacklisted) { - continue 'outer; + let size_preferences = ["scalable", "128x128", "96x96", "72x72", "64x64", "48x48", "32x32"] + .into_iter() + .map(OsStr::new) + .collect(); + + Self { + icon_folders, + icon_cache: HashMap::new(), + size_preferences, + } + } + + fn find_icon(&mut self, icon_name: &str) -> Option> { + if let Some(icon_path) = self.icon_cache.get(icon_name) { + return Some(icon_path.clone()); + } + + for folder in &self.icon_folders { + let pattern = format!("{}/*/apps/*/{}.*", folder, icon_name); + + let mut entries: Vec<_> = glob::glob(&pattern) + .expect("Bad glob pattern!") + .filter_map(Result::ok) + .collect(); + + log::warn!("Looking for '{pattern}' resulted in {} entries.", entries.len()); + + // sort by SIZE_PREFERENCES + entries.sort_by_key(|path| { + path + .components() + .rev() + .nth(1) // ← /apps/<*SIZE*>/filename.ext + .map(|c| c.as_os_str()) + .and_then(|size| { + log::warn!("looking for {size:?} in size preferences."); + self.size_preferences.iter().position(|&p| p == size) + }) + .unwrap_or(usize::MAX) + }); + + if let Some(first) = entries.into_iter().next() { + let rc: Rc = first.to_string_lossy().into(); + log::warn!("Found icon for {icon_name} at {rc}"); + self.icon_cache.insert(icon_name.to_string(), rc.clone()); + return Some(rc); } } - - let (exec_path, exec_args) = match exec.split_once(" ") { - Some((left, right)) => ( - String::from(left), - right - .split(" ") - .filter(|arg| !arg.starts_with('%')) // exclude arguments like "%f" - .map(String::from) - .collect(), - ), - None => (exec, Vec::new()), - }; - - let icon_path = app_entry - .icon() - .and_then(|icon_name| theme.get_with_size(&icon_name, ICON_SIZE)) - .and_then(|path_buf| path_buf.into_os_string().into_string().ok()); - - let categories = app_entry.categories().map_or_else(Vec::default, |inner| { - inner - .into_iter() - .filter(|s| !(s.is_empty() || CATEGORY_TYPE_BLACKLIST.contains(&s.as_str()))) - .collect() - }); - - let entry = DesktopEntry { - app_name: name, - categories, - exec_path, - exec_args, - icon_path, - }; - - res.push(entry); + None } - Ok(res) -} + pub fn find_entries(&mut self) -> anyhow::Result> { + let mut res = Vec::::new(); -impl DesktopEntry { - pub fn to_desktop_file(&self) -> DesktopFile { - DesktopFile { - categories: self.categories.clone(), - exec_args: self.exec_args.clone(), - exec_path: self.exec_path.clone(), - icon: self.icon_path.clone(), - name: self.app_name.clone(), + 'app_entries: for app_entry in ApplicationEntry::all() { + let Some(app_entry_id) = app_entry.id() else { + log::warn!( + "No desktop entry id for application \"{}\"", + app_entry.name().as_deref().unwrap_or("") + ); + continue; + }; + + let Some(name) = app_entry.name() else { + log::warn!("No Name on desktop entry {}", app_entry_id); + continue; + }; + + let Some(exec) = app_entry.exec() else { + log::warn!("No Exec on desktop entry {}", app_entry_id); + continue; + }; + + if app_entry.no_display() || app_entry.is_hidden() || app_entry.terminal() { + continue; + } + + for blacklisted in CMD_BLACKLIST { + if exec.contains(blacklisted) { + continue 'app_entries; + } + } + + let (exec_path, exec_args) = match exec.split_once(" ") { + Some((left, right)) => (left.into(), right.into()), + None => (exec.into(), "".into()), + }; + + let icon_path = app_entry.icon().and_then(|icon_name| self.find_icon(&icon_name)); + + for cat in app_entry.categories().unwrap_or_default() { + if CATEGORY_TYPE_BLACKLIST.contains(&cat.as_str()) { + continue 'app_entries; + } + } + + let entry = DesktopEntry { + app_name: name.into(), + exec_path, + exec_args, + icon_path, + }; + + res.push(entry); } + + Ok(res) } } diff --git a/dash-frontend/src/util/various.rs b/dash-frontend/src/util/various.rs index 6561431..a67f52d 100644 --- a/dash-frontend/src/util/various.rs +++ b/dash-frontend/src/util/various.rs @@ -18,12 +18,12 @@ use crate::util::desktop_finder; // the compiler wants to scream #[allow(irrefutable_let_patterns)] -pub fn get_desktop_file_icon_path(desktop_file: &desktop_finder::DesktopFile) -> AssetPathOwned { +pub fn get_desktop_file_icon_path(desktop_file: &desktop_finder::DesktopEntry) -> AssetPathOwned { /* FIXME: why is the compiler complaining about trailing irrefutable patterns there?!?! looking at the PathBuf::from_str implementation, it always returns Ok() and it's inline, maybe that's why. */ - if let Some(icon) = &desktop_file.icon + if let Some(icon) = &desktop_file.icon_path && let Ok(path) = PathBuf::from_str(icon) { return AssetPathOwned::File(path); diff --git a/dash-frontend/src/views/app_launcher.rs b/dash-frontend/src/views/app_launcher.rs index a1df3f2..c09d061 100644 --- a/dash-frontend/src/views/app_launcher.rs +++ b/dash-frontend/src/views/app_launcher.rs @@ -85,12 +85,12 @@ impl View { label_exec.set_text_simple( &mut params.globals.get(), - Translation::from_raw_text_string(params.entry.app_name.clone()), + Translation::from_raw_text_rc(params.entry.app_name.clone()), ); label_args.set_text_simple( &mut params.globals.get(), - Translation::from_raw_text_string(params.entry.exec_args.join(" ")), + Translation::from_raw_text_rc(params.entry.exec_args.clone()), ); } @@ -103,7 +103,7 @@ impl View { // app icon if let Some(icon_path) = ¶ms.entry.icon_path { let mut template_params: HashMap, Rc> = HashMap::new(); - template_params.insert("path".into(), icon_path.as_str().into()); + template_params.insert("path".into(), icon_path.clone()); state.instantiate_template( doc_params, "ApplicationIcon", @@ -225,27 +225,23 @@ impl View { env.push("ELECTRON_OZONE_PLATFORM_HINT=wayland".into()); } - // TODO: refactor this after we ditch old wayvr-dashboard completely - let desktop_file = params.application.to_desktop_file(); - let mut userdata = HashMap::::new(); - userdata.insert("desktop_file".into(), serde_json::to_string(&desktop_file)?); - - let exec_args_str = desktop_file.exec_args.join(" "); - let args = match params.run_mode { - RunMode::Cage => format!("-- {} {}", desktop_file.exec_path, exec_args_str), - RunMode::Wayland => exec_args_str, + RunMode::Cage => format!("-- {} {}", params.application.exec_path, params.application.exec_args), + RunMode::Wayland => params.application.exec_args.to_string(), }; let exec = match params.run_mode { - RunMode::Cage => "cage", - RunMode::Wayland => &desktop_file.name, + RunMode::Cage => "cage".to_string(), + RunMode::Wayland => params.application.exec_path.to_string(), }; + let mut userdata = HashMap::new(); + userdata.insert("desktop-entry".to_string(), serde_json::to_string(params.application)?); + params.interface.process_launch(WvrProcessLaunchParams { env, - exec: String::from(exec), - name: desktop_file.name, + exec, + name: params.application.app_name.to_string(), args, userdata, })?; diff --git a/dash-frontend/src/views/process_list.rs b/dash-frontend/src/views/process_list.rs index efe472a..4d831d6 100644 --- a/dash-frontend/src/views/process_list.rs +++ b/dash-frontend/src/views/process_list.rs @@ -15,9 +15,9 @@ use wgui::{ taffy::{self, prelude::length}, task::Tasks, widget::{ - ConstructEssentials, div::WidgetDiv, label::{WidgetLabel, WidgetLabelParams}, + ConstructEssentials, }, }; use wlx_common::dash_interface::BoxDashInterface; @@ -89,13 +89,13 @@ impl View { } } -fn get_desktop_file_from_process(process: &packet_server::WvrProcess) -> Option { +fn get_desktop_entry_from_process(process: &packet_server::WvrProcess) -> Option { // TODO: refactor this after we ditch old wayvr-dashboard completely - let Some(dfile_str) = process.userdata.get("desktop_file") else { + let Some(dfile_str) = process.userdata.get("desktop-entry") else { return None; }; - let Ok(desktop_file) = serde_json::from_str::(dfile_str) else { + let Ok(desktop_file) = serde_json::from_str::(dfile_str) else { debug_assert!(false); // invalid json??? return None; }; @@ -144,7 +144,7 @@ fn construct_process_entry( }, )?; - if let Some(desktop_file) = get_desktop_file_from_process(process) { + if let Some(desktop_file) = get_desktop_entry_from_process(process) { // desktop file icon and process name util::various::mount_simple_sprite_square( globals, @@ -158,7 +158,7 @@ fn construct_process_entry( globals, ess.layout, cell.id, - Translation::from_raw_text_string(desktop_file.name.clone()), + Translation::from_raw_text_rc(desktop_file.app_name.clone()), )?; } else { // just show a process name