refactor: simplify theme usage and enhance AppsPlugin structure

This commit is contained in:
2026-03-15 16:30:35 +01:00
parent 1ac9dde347
commit 6780444caa
3 changed files with 76 additions and 47 deletions

View File

@@ -9,7 +9,7 @@ use iced::{
use k_launcher_kernel::{Kernel, SearchResult}; use k_launcher_kernel::{Kernel, SearchResult};
use crate::theme::AeroColors; use crate::theme;
static INPUT_ID: std::sync::LazyLock<iced::widget::Id> = static INPUT_ID: std::sync::LazyLock<iced::widget::Id> =
std::sync::LazyLock::new(|| iced::widget::Id::new("search")); std::sync::LazyLock::new(|| iced::widget::Id::new("search"));
@@ -89,7 +89,7 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task<Message> {
} }
fn view(state: &KLauncherApp) -> Element<'_, Message> { fn view(state: &KLauncherApp) -> Element<'_, Message> {
let colors = AeroColors::standard(); let colors = &*theme::AERO;
let search_bar = text_input("Search...", &state.query) let search_bar = text_input("Search...", &state.query)
.id(INPUT_ID.clone()) .id(INPUT_ID.clone())

View File

@@ -9,6 +9,9 @@ pub struct AeroColors {
pub border_cyan: Color, pub border_cyan: Color,
} }
pub static AERO: std::sync::LazyLock<AeroColors> =
std::sync::LazyLock::new(AeroColors::standard);
impl AeroColors { impl AeroColors {
pub fn standard() -> Self { pub fn standard() -> Self {
Self { Self {

View File

@@ -56,15 +56,55 @@ pub trait DesktopEntrySource: Send + Sync {
fn entries(&self) -> Vec<DesktopEntry>; fn entries(&self) -> Vec<DesktopEntry>;
} }
// --- Plugin --- // --- Cached entry (pre-computed at construction) ---
pub struct AppsPlugin<S: DesktopEntrySource> { struct CachedEntry {
source: S, name: AppName,
name_lc: String,
icon: Option<String>,
on_execute: Arc<dyn Fn() + Send + Sync>,
} }
impl<S: DesktopEntrySource> AppsPlugin<S> { // --- Plugin ---
pub fn new(source: S) -> Self {
Self { source } pub struct AppsPlugin {
entries: Vec<CachedEntry>,
}
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<String> {
candidates.into_iter().find(|p| Path::new(p).exists()) candidates.into_iter().find(|p| Path::new(p).exists())
} }
fn score_match(name: &str, query: &str) -> Option<u32> { fn score_match(name_lc: &str, query_lc: &str) -> Option<u32> {
let name_lc = name.to_lowercase();
let query_lc = query.to_lowercase();
if name_lc == query_lc { if name_lc == query_lc {
Some(100) Some(100)
} else if name_lc.starts_with(&query_lc) { } else if name_lc.starts_with(query_lc) {
Some(80) Some(80)
} else if name_lc.contains(&query_lc) { } else if name_lc.contains(query_lc) {
Some(60) Some(60)
} else { } else {
None None
@@ -96,7 +134,7 @@ fn score_match(name: &str, query: &str) -> Option<u32> {
} }
#[async_trait] #[async_trait]
impl<S: DesktopEntrySource> Plugin for AppsPlugin<S> { impl Plugin for AppsPlugin {
fn name(&self) -> PluginName { fn name(&self) -> PluginName {
"apps" "apps"
} }
@@ -105,34 +143,17 @@ impl<S: DesktopEntrySource> Plugin for AppsPlugin<S> {
if query.is_empty() { if query.is_empty() {
return vec![]; return vec![];
} }
self.source let query_lc = query.to_lowercase();
.entries() self.entries
.into_iter() .iter()
.filter_map(|entry| { .filter_map(|e| {
score_match(entry.name.as_str(), query).map(|score| { score_match(&e.name_lc, &query_lc).map(|score| SearchResult {
let exec = entry.exec.clone(); id: ResultId::new(format!("app-{}", e.name.as_str())),
let icon = entry.icon.as_ref().and_then(|p| resolve_icon_path(p.as_str())); title: ResultTitle::new(e.name.as_str()),
SearchResult {
id: ResultId::new(format!("app-{}", entry.name.as_str())),
title: ResultTitle::new(entry.name.as_str()),
description: None, description: None,
icon, icon: e.icon.clone(),
score: Score::new(score), score: Score::new(score),
on_execute: Arc::new(move || { on_execute: Arc::clone(&e.on_execute),
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()
};
}
}),
}
}) })
}) })
.collect() .collect()
@@ -188,7 +209,7 @@ fn parse_desktop_file(path: &Path) -> Option<DesktopEntry> {
let mut name: Option<String> = None; let mut name: Option<String> = None;
let mut exec: Option<String> = None; let mut exec: Option<String> = None;
let mut icon: Option<String> = None; let mut icon: Option<String> = None;
let mut entry_type: Option<String> = None; let mut is_application = false;
let mut no_display = false; let mut no_display = false;
for line in content.lines() { for line in content.lines() {
@@ -209,22 +230,27 @@ fn parse_desktop_file(path: &Path) -> Option<DesktopEntry> {
"Name" if name.is_none() => name = Some(value.trim().to_string()), "Name" if name.is_none() => name = Some(value.trim().to_string()),
"Exec" if exec.is_none() => exec = 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()), "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"), "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; return None;
} }
let exec_clean: String = exec? let exec_clean: String = exec?
.split_whitespace() .split_whitespace()
.filter(|s| !s.starts_with('%')) .filter(|s| !s.starts_with('%'))
.collect::<Vec<_>>() .fold(String::new(), |mut acc, s| {
.join(" "); if !acc.is_empty() {
acc.push(' ');
}
acc.push_str(s);
acc
});
Some(DesktopEntry { Some(DesktopEntry {
name: AppName::new(name?), name: AppName::new(name?),