From 6780444caaad5497ce07d4bc09e52d857878b1b8 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Sun, 15 Mar 2026 16:30:35 +0100 Subject: [PATCH] refactor: simplify theme usage and enhance AppsPlugin structure --- crates/k-launcher-ui/src/app.rs | 4 +- crates/k-launcher-ui/src/theme.rs | 3 + crates/plugins/plugin-apps/src/lib.rs | 116 ++++++++++++++++---------- 3 files changed, 76 insertions(+), 47 deletions(-) diff --git a/crates/k-launcher-ui/src/app.rs b/crates/k-launcher-ui/src/app.rs index f93c7bd..70704d8 100644 --- a/crates/k-launcher-ui/src/app.rs +++ b/crates/k-launcher-ui/src/app.rs @@ -9,7 +9,7 @@ use iced::{ use k_launcher_kernel::{Kernel, SearchResult}; -use crate::theme::AeroColors; +use crate::theme; static INPUT_ID: std::sync::LazyLock = std::sync::LazyLock::new(|| iced::widget::Id::new("search")); @@ -89,7 +89,7 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task { } fn view(state: &KLauncherApp) -> Element<'_, Message> { - let colors = AeroColors::standard(); + let colors = &*theme::AERO; let search_bar = text_input("Search...", &state.query) .id(INPUT_ID.clone()) diff --git a/crates/k-launcher-ui/src/theme.rs b/crates/k-launcher-ui/src/theme.rs index ab56fe1..2330462 100644 --- a/crates/k-launcher-ui/src/theme.rs +++ b/crates/k-launcher-ui/src/theme.rs @@ -9,6 +9,9 @@ pub struct AeroColors { pub border_cyan: Color, } +pub static AERO: std::sync::LazyLock = + std::sync::LazyLock::new(AeroColors::standard); + impl AeroColors { pub fn standard() -> Self { Self { diff --git a/crates/plugins/plugin-apps/src/lib.rs b/crates/plugins/plugin-apps/src/lib.rs index f5f0c7e..c621200 100644 --- a/crates/plugins/plugin-apps/src/lib.rs +++ b/crates/plugins/plugin-apps/src/lib.rs @@ -56,15 +56,55 @@ pub trait DesktopEntrySource: Send + Sync { fn entries(&self) -> Vec; } -// --- Plugin --- +// --- Cached entry (pre-computed at construction) --- -pub struct AppsPlugin { - source: S, +struct CachedEntry { + name: AppName, + name_lc: String, + icon: Option, + on_execute: Arc, } -impl AppsPlugin { - pub fn new(source: S) -> Self { - Self { source } +// --- Plugin --- + +pub struct AppsPlugin { + entries: Vec, +} + +impl AppsPlugin { + pub fn new(source: impl DesktopEntrySource) -> Self { + let entries = source + .entries() + .into_iter() + .map(|e| { + let name_lc = e.name.as_str().to_lowercase(); + let icon = e.icon.as_ref().and_then(|p| resolve_icon_path(p.as_str())); + let exec = e.exec.clone(); + CachedEntry { + name_lc, + icon, + on_execute: Arc::new(move || { + let parts: Vec<&str> = exec.as_str().split_whitespace().collect(); + if let Some((cmd, args)) = parts.split_first() { + let _ = unsafe { + Command::new(cmd) + .args(args) + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .pre_exec(|| { + libc::setsid(); + Ok(()) + }) + .spawn() + }; + } + }), + name: e.name, + } + }) + .collect(); + Self { entries } } } @@ -81,14 +121,12 @@ fn resolve_icon_path(name: &str) -> Option { candidates.into_iter().find(|p| Path::new(p).exists()) } -fn score_match(name: &str, query: &str) -> Option { - let name_lc = name.to_lowercase(); - let query_lc = query.to_lowercase(); +fn score_match(name_lc: &str, query_lc: &str) -> Option { if name_lc == query_lc { Some(100) - } else if name_lc.starts_with(&query_lc) { + } else if name_lc.starts_with(query_lc) { Some(80) - } else if name_lc.contains(&query_lc) { + } else if name_lc.contains(query_lc) { Some(60) } else { None @@ -96,7 +134,7 @@ fn score_match(name: &str, query: &str) -> Option { } #[async_trait] -impl Plugin for AppsPlugin { +impl Plugin for AppsPlugin { fn name(&self) -> PluginName { "apps" } @@ -105,34 +143,17 @@ impl Plugin for AppsPlugin { if query.is_empty() { return vec![]; } - self.source - .entries() - .into_iter() - .filter_map(|entry| { - score_match(entry.name.as_str(), query).map(|score| { - let exec = entry.exec.clone(); - let icon = entry.icon.as_ref().and_then(|p| resolve_icon_path(p.as_str())); - SearchResult { - id: ResultId::new(format!("app-{}", entry.name.as_str())), - title: ResultTitle::new(entry.name.as_str()), - description: None, - icon, - score: Score::new(score), - on_execute: Arc::new(move || { - let parts: Vec<&str> = exec.as_str().split_whitespace().collect(); - if let Some((cmd, args)) = parts.split_first() { - let _ = unsafe { - Command::new(cmd) - .args(args) - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()) - .pre_exec(|| { libc::setsid(); Ok(()) }) - .spawn() - }; - } - }), - } + let query_lc = query.to_lowercase(); + self.entries + .iter() + .filter_map(|e| { + score_match(&e.name_lc, &query_lc).map(|score| SearchResult { + id: ResultId::new(format!("app-{}", e.name.as_str())), + title: ResultTitle::new(e.name.as_str()), + description: None, + icon: e.icon.clone(), + score: Score::new(score), + on_execute: Arc::clone(&e.on_execute), }) }) .collect() @@ -188,7 +209,7 @@ fn parse_desktop_file(path: &Path) -> Option { let mut name: Option = None; let mut exec: Option = None; let mut icon: Option = None; - let mut entry_type: Option = None; + let mut is_application = false; let mut no_display = false; for line in content.lines() { @@ -209,22 +230,27 @@ fn parse_desktop_file(path: &Path) -> Option { "Name" if name.is_none() => name = Some(value.trim().to_string()), "Exec" if exec.is_none() => exec = Some(value.trim().to_string()), "Icon" if icon.is_none() => icon = Some(value.trim().to_string()), - "Type" if entry_type.is_none() => entry_type = Some(value.trim().to_string()), + "Type" if !is_application => is_application = value.trim() == "Application", "NoDisplay" => no_display = value.trim().eq_ignore_ascii_case("true"), _ => {} } } } - if entry_type.as_deref() != Some("Application") || no_display { + if !is_application || no_display { return None; } let exec_clean: String = exec? .split_whitespace() .filter(|s| !s.starts_with('%')) - .collect::>() - .join(" "); + .fold(String::new(), |mut acc, s| { + if !acc.is_empty() { + acc.push(' '); + } + acc.push_str(s); + acc + }); Some(DesktopEntry { name: AppName::new(name?),