From ff9b2b571222f5ceb5629b1f889e44bf350b818b Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Wed, 18 Mar 2026 13:45:48 +0100 Subject: [PATCH] fix(review): bugs, arch violations, design smells P1 bugs: - unix_launcher: shell_split respects quoted args (was split_whitespace) - plugin-host: 5s timeout on external plugin search - ui: handle engine init panic, wire error state - ui-egui: read window config instead of always using defaults - plugin-url: use OpenPath action instead of SpawnProcess+xdg-open Architecture: - remove WindowConfig (mirror of WindowCfg); use WindowCfg directly - remove on_select closure from SearchResult (domain leakage) - remove LaunchAction::Custom; add Plugin::on_selected + SearchEngine::on_selected - apps: record frecency via on_selected instead of embedded closure Design smells: - frecency: extract decay_factor helper, write outside mutex - apps: remove cfg(test) cache_path hack; add new_for_test ctor - apps: stable ResultId using name+exec to prevent collision - files: stable ResultId using full path instead of index - plugin-host: remove k-launcher-os-bridge dep (WindowConfig gone) --- Cargo.lock | 2 - crates/k-launcher-kernel/src/lib.rs | 17 ++++- crates/k-launcher-os-bridge/Cargo.toml | 1 - crates/k-launcher-os-bridge/src/lib.rs | 21 ----- .../k-launcher-os-bridge/src/unix_launcher.rs | 70 ++++++++++++++++- crates/k-launcher-plugin-host/Cargo.toml | 2 +- crates/k-launcher-plugin-host/src/lib.rs | 11 ++- crates/k-launcher-ui-egui/Cargo.toml | 1 - crates/k-launcher-ui-egui/src/app.rs | 15 ++-- crates/k-launcher-ui-egui/src/lib.rs | 3 +- crates/k-launcher-ui/src/app.rs | 30 +++++--- crates/k-launcher/src/main_egui.rs | 3 +- crates/plugins/plugin-apps/src/frecency.rs | 50 ++++++------ crates/plugins/plugin-apps/src/lib.rs | 76 +++++++++---------- crates/plugins/plugin-calc/src/lib.rs | 1 - crates/plugins/plugin-cmd/src/lib.rs | 1 - crates/plugins/plugin-files/src/lib.rs | 6 +- crates/plugins/plugin-url/src/main.rs | 12 +-- 18 files changed, 189 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7ee4503..541b0ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2082,7 +2082,6 @@ dependencies = [ name = "k-launcher-os-bridge" version = "0.1.0" dependencies = [ - "k-launcher-config", "k-launcher-kernel", "libc", ] @@ -2118,7 +2117,6 @@ dependencies = [ "egui", "k-launcher-config", "k-launcher-kernel", - "k-launcher-os-bridge", "tokio", ] diff --git a/crates/k-launcher-kernel/src/lib.rs b/crates/k-launcher-kernel/src/lib.rs index 497a29b..b387376 100644 --- a/crates/k-launcher-kernel/src/lib.rs +++ b/crates/k-launcher-kernel/src/lib.rs @@ -50,7 +50,6 @@ pub enum LaunchAction { SpawnInTerminal(String), OpenPath(String), CopyToClipboard(String), - Custom(Arc), } // --- AppLauncher port trait --- @@ -68,7 +67,6 @@ pub struct SearchResult { pub icon: Option, pub score: Score, pub action: LaunchAction, - pub on_select: Option>, } impl std::fmt::Debug for SearchResult { @@ -88,6 +86,7 @@ impl std::fmt::Debug for SearchResult { pub trait Plugin: Send + Sync { fn name(&self) -> &str; async fn search(&self, query: &str) -> Vec; + fn on_selected(&self, _id: &ResultId) {} } // --- SearchEngine port trait --- @@ -95,6 +94,7 @@ pub trait Plugin: Send + Sync { #[async_trait] pub trait SearchEngine: Send + Sync { async fn search(&self, query: &str) -> Vec; + fn on_selected(&self, id: &ResultId); } // --- NullSearchEngine --- @@ -106,6 +106,7 @@ impl SearchEngine for NullSearchEngine { async fn search(&self, _query: &str) -> Vec { vec![] } + fn on_selected(&self, _id: &ResultId) {} } // --- Kernel (Application use case) --- @@ -123,6 +124,12 @@ impl Kernel { } } + pub fn on_selected(&self, id: &ResultId) { + for plugin in &self.plugins { + plugin.on_selected(id); + } + } + pub async fn search(&self, query: &str) -> Vec { use futures::FutureExt; use std::panic::AssertUnwindSafe; @@ -154,6 +161,9 @@ impl SearchEngine for Kernel { async fn search(&self, query: &str) -> Vec { self.search(query).await } + fn on_selected(&self, id: &ResultId) { + self.on_selected(id); + } } // --- Tests --- @@ -188,8 +198,7 @@ mod tests { description: None, icon: None, score: Score::new(*score), - action: LaunchAction::Custom(Arc::new(|| {})), - on_select: None, + action: LaunchAction::SpawnProcess("mock".to_string()), }) .collect() } diff --git a/crates/k-launcher-os-bridge/Cargo.toml b/crates/k-launcher-os-bridge/Cargo.toml index 21ee736..6c82834 100644 --- a/crates/k-launcher-os-bridge/Cargo.toml +++ b/crates/k-launcher-os-bridge/Cargo.toml @@ -4,6 +4,5 @@ version = "0.1.0" edition = "2024" [dependencies] -k-launcher-config = { path = "../k-launcher-config" } k-launcher-kernel = { path = "../k-launcher-kernel" } libc = "0.2" diff --git a/crates/k-launcher-os-bridge/src/lib.rs b/crates/k-launcher-os-bridge/src/lib.rs index 3c4b717..cf60b50 100644 --- a/crates/k-launcher-os-bridge/src/lib.rs +++ b/crates/k-launcher-os-bridge/src/lib.rs @@ -1,23 +1,2 @@ mod unix_launcher; - pub use unix_launcher::UnixAppLauncher; - -pub struct WindowConfig { - pub width: f32, - pub height: f32, - pub decorations: bool, - pub transparent: bool, - pub resizable: bool, -} - -impl WindowConfig { - pub fn from_cfg(w: &k_launcher_config::WindowCfg) -> Self { - Self { - width: w.width, - height: w.height, - decorations: w.decorations, - transparent: w.transparent, - resizable: w.resizable, - } - } -} diff --git a/crates/k-launcher-os-bridge/src/unix_launcher.rs b/crates/k-launcher-os-bridge/src/unix_launcher.rs index 9c8173e..8561e97 100644 --- a/crates/k-launcher-os-bridge/src/unix_launcher.rs +++ b/crates/k-launcher-os-bridge/src/unix_launcher.rs @@ -3,6 +3,29 @@ use std::process::{Command, Stdio}; use k_launcher_kernel::{AppLauncher, LaunchAction}; +fn shell_split(cmd: &str) -> Vec { + let mut tokens = Vec::new(); + let mut current = String::new(); + let mut in_quotes = false; + + for ch in cmd.chars() { + match ch { + '"' => in_quotes = !in_quotes, + ' ' | '\t' if !in_quotes => { + if !current.is_empty() { + tokens.push(current.clone()); + current.clear(); + } + } + _ => current.push(ch), + } + } + if !current.is_empty() { + tokens.push(current); + } + tokens +} + fn parse_term_cmd(s: &str) -> (String, Vec) { let mut parts = s.split_whitespace(); let bin = parts.next().unwrap_or("").to_string(); @@ -69,7 +92,7 @@ impl AppLauncher for UnixAppLauncher { fn execute(&self, action: &LaunchAction) { match action { LaunchAction::SpawnProcess(cmd) => { - let parts: Vec<&str> = cmd.split_whitespace().collect(); + let parts = shell_split(cmd); if let Some((bin, args)) = parts.split_first() { let _ = unsafe { Command::new(bin) @@ -121,7 +144,50 @@ impl AppLauncher for UnixAppLauncher { } } } - LaunchAction::Custom(f) => f(), } } } + +#[cfg(test)] +mod tests { + use super::shell_split; + + #[test] + fn split_simple() { + assert_eq!(shell_split("firefox"), vec!["firefox"]); + } + + #[test] + fn split_with_args() { + assert_eq!( + shell_split("firefox --new-window"), + vec!["firefox", "--new-window"] + ); + } + + #[test] + fn split_quoted_path() { + assert_eq!( + shell_split(r#""My App" --flag"#), + vec!["My App", "--flag"] + ); + } + + #[test] + fn split_quoted_with_spaces() { + assert_eq!( + shell_split(r#"env "FOO BAR" baz"#), + vec!["env", "FOO BAR", "baz"] + ); + } + + #[test] + fn split_empty() { + assert!(shell_split("").is_empty()); + } + + #[test] + fn split_extra_whitespace() { + assert_eq!(shell_split(" a b "), vec!["a", "b"]); + } +} diff --git a/crates/k-launcher-plugin-host/Cargo.toml b/crates/k-launcher-plugin-host/Cargo.toml index d4cab18..92ba544 100644 --- a/crates/k-launcher-plugin-host/Cargo.toml +++ b/crates/k-launcher-plugin-host/Cargo.toml @@ -12,5 +12,5 @@ async-trait = { workspace = true } k-launcher-kernel = { path = "../k-launcher-kernel" } serde = { workspace = true } serde_json = { workspace = true } -tokio = { workspace = true, features = ["process", "io-util", "sync"] } +tokio = { workspace = true, features = ["process", "io-util", "sync", "time"] } tracing = { workspace = true } diff --git a/crates/k-launcher-plugin-host/src/lib.rs b/crates/k-launcher-plugin-host/src/lib.rs index e601d75..0903385 100644 --- a/crates/k-launcher-plugin-host/src/lib.rs +++ b/crates/k-launcher-plugin-host/src/lib.rs @@ -105,7 +105,15 @@ impl Plugin for ExternalPlugin { } let result = match guard.as_mut() { - Some(io) => do_search(io, query).await, + Some(io) => tokio::time::timeout( + std::time::Duration::from_secs(5), + do_search(io, query), + ) + .await + .unwrap_or_else(|_| { + tracing::warn!("plugin {} search timed out", self.name); + Err("timeout".into()) + }), None => unreachable!(), }; @@ -125,7 +133,6 @@ impl Plugin for ExternalPlugin { } ExternalAction::OpenPath { path } => LaunchAction::OpenPath(path), }, - on_select: None, }) .collect(), Err(e) => { diff --git a/crates/k-launcher-ui-egui/Cargo.toml b/crates/k-launcher-ui-egui/Cargo.toml index 62be894..9c65657 100644 --- a/crates/k-launcher-ui-egui/Cargo.toml +++ b/crates/k-launcher-ui-egui/Cargo.toml @@ -12,5 +12,4 @@ eframe = { version = "0.31", default-features = false, features = ["default_font egui = "0.31" k-launcher-config = { path = "../k-launcher-config" } k-launcher-kernel = { path = "../k-launcher-kernel" } -k-launcher-os-bridge = { path = "../k-launcher-os-bridge" } tokio = { workspace = true } diff --git a/crates/k-launcher-ui-egui/src/app.rs b/crates/k-launcher-ui-egui/src/app.rs index 4f46a3e..5bfa1ff 100644 --- a/crates/k-launcher-ui-egui/src/app.rs +++ b/crates/k-launcher-ui-egui/src/app.rs @@ -2,7 +2,6 @@ use std::sync::{Arc, mpsc}; use egui::{Color32, Key, ViewportCommand}; use k_launcher_kernel::{AppLauncher, SearchEngine, SearchResult}; -use k_launcher_os_bridge::WindowConfig; const BG: Color32 = Color32::from_rgba_premultiplied(20, 20, 30, 230); const BORDER_COLOR: Color32 = Color32::from_rgb(229, 125, 33); @@ -83,9 +82,7 @@ impl eframe::App for KLauncherApp { if launch_selected { if let Some(result) = self.results.get(self.selected) { - if let Some(on_select) = &result.on_select { - on_select(); - } + self.engine.on_selected(&result.id); self.launcher.execute(&result.action); } ctx.send_viewport_cmd(ViewportCommand::Close); @@ -166,17 +163,17 @@ impl eframe::App for KLauncherApp { pub fn run( engine: Arc, launcher: Arc, + window_cfg: &k_launcher_config::WindowCfg, ) -> Result<(), eframe::Error> { - let wc = WindowConfig::from_cfg(&k_launcher_config::WindowCfg::default()); let rt = tokio::runtime::Runtime::new().expect("tokio runtime"); let handle = rt.handle().clone(); let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() - .with_inner_size([wc.width, wc.height]) - .with_decorations(wc.decorations) - .with_transparent(wc.transparent) - .with_resizable(wc.resizable) + .with_inner_size([window_cfg.width, window_cfg.height]) + .with_decorations(window_cfg.decorations) + .with_transparent(window_cfg.transparent) + .with_resizable(window_cfg.resizable) .with_always_on_top(), ..Default::default() }; diff --git a/crates/k-launcher-ui-egui/src/lib.rs b/crates/k-launcher-ui-egui/src/lib.rs index ad4f3a5..2f2d848 100644 --- a/crates/k-launcher-ui-egui/src/lib.rs +++ b/crates/k-launcher-ui-egui/src/lib.rs @@ -7,6 +7,7 @@ use k_launcher_kernel::{AppLauncher, SearchEngine}; pub fn run( engine: Arc, launcher: Arc, + window_cfg: &k_launcher_config::WindowCfg, ) -> Result<(), eframe::Error> { - app::run(engine, launcher) + app::run(engine, launcher, window_cfg) } diff --git a/crates/k-launcher-ui/src/app.rs b/crates/k-launcher-ui/src/app.rs index 53f9de0..6e15098 100644 --- a/crates/k-launcher-ui/src/app.rs +++ b/crates/k-launcher-ui/src/app.rs @@ -9,7 +9,6 @@ use iced::{ use k_launcher_config::AppearanceCfg; use k_launcher_kernel::{AppLauncher, NullSearchEngine, SearchEngine, SearchResult}; -use k_launcher_os_bridge::WindowConfig; static INPUT_ID: std::sync::LazyLock = std::sync::LazyLock::new(|| iced::widget::Id::new("search")); @@ -63,6 +62,7 @@ pub enum Message { ResultsReady(u64, Arc>), KeyPressed(KeyEvent), EngineReady(EngineHandle), + EngineInitFailed(String), } fn update(state: &mut KLauncherApp, message: Message) -> Task { @@ -88,6 +88,10 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task { } Task::none() } + Message::EngineInitFailed(msg) => { + state.error = Some(msg); + Task::none() + } Message::EngineReady(handle) => { state.engine = handle.0; if !state.query.is_empty() { @@ -121,9 +125,7 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task { } Named::Enter => { if let Some(result) = state.results.get(state.selected) { - if let Some(on_select) = &result.on_select { - on_select(); - } + state.engine.on_selected(&result.id); state.launcher.execute(&result.action); } std::process::exit(0); @@ -277,7 +279,6 @@ pub fn run( window_cfg: &k_launcher_config::WindowCfg, appearance_cfg: AppearanceCfg, ) -> iced::Result { - let wc = WindowConfig::from_cfg(window_cfg); iced::application( move || { let app = KLauncherApp::new( @@ -288,8 +289,15 @@ pub fn run( let focus = iced::widget::operation::focus(INPUT_ID.clone()); let ef = engine_factory.clone(); let init = Task::perform( - async move { tokio::task::spawn_blocking(move || ef()).await.unwrap() }, - |e| Message::EngineReady(EngineHandle(e)), + async move { + tokio::task::spawn_blocking(move || ef()) + .await + .map_err(|e| format!("Engine init failed: {e}")) + }, + |result| match result { + Ok(e) => Message::EngineReady(EngineHandle(e)), + Err(msg) => Message::EngineInitFailed(msg), + }, ); (app, Task::batch([focus, init])) }, @@ -299,11 +307,11 @@ pub fn run( .title("K-Launcher") .subscription(subscription) .window(window::Settings { - size: Size::new(wc.width, wc.height), + size: Size::new(window_cfg.width, window_cfg.height), position: window::Position::Centered, - decorations: wc.decorations, - transparent: wc.transparent, - resizable: wc.resizable, + decorations: window_cfg.decorations, + transparent: window_cfg.transparent, + resizable: window_cfg.resizable, ..Default::default() }) .run() diff --git a/crates/k-launcher/src/main_egui.rs b/crates/k-launcher/src/main_egui.rs index c72bc21..f632b5c 100644 --- a/crates/k-launcher/src/main_egui.rs +++ b/crates/k-launcher/src/main_egui.rs @@ -10,6 +10,7 @@ use plugin_cmd::CmdPlugin; use plugin_files::FilesPlugin; fn main() -> Result<(), Box> { + let cfg = k_launcher_config::load(); let launcher = Arc::new(UnixAppLauncher::new()); let frecency = FrecencyStore::load(); let kernel: Arc = Arc::new(Kernel::new( @@ -21,6 +22,6 @@ fn main() -> Result<(), Box> { ], 8, )); - k_launcher_ui_egui::run(kernel, launcher)?; + k_launcher_ui_egui::run(kernel, launcher, &cfg.window)?; Ok(()) } diff --git a/crates/plugins/plugin-apps/src/frecency.rs b/crates/plugins/plugin-apps/src/frecency.rs index fe997fb..4f408b2 100644 --- a/crates/plugins/plugin-apps/src/frecency.rs +++ b/crates/plugins/plugin-apps/src/frecency.rs @@ -55,17 +55,17 @@ impl FrecencyStore { .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_secs(); - let mut data = self.data.lock().unwrap(); - let entry = data.entry(id.to_string()).or_insert(Entry { - count: 0, - last_used: 0, - }); - entry.count += 1; - entry.last_used = now; - if let Some(parent) = self.path.parent() { - let _ = std::fs::create_dir_all(parent); - } - if let Ok(json) = serde_json::to_string(&*data) { + let json = { + let mut data = self.data.lock().unwrap(); + let entry = data.entry(id.to_string()).or_insert(Entry { count: 0, last_used: 0 }); + entry.count += 1; + entry.last_used = now; + serde_json::to_string(&*data).ok() + }; // lock released here + if let Some(json) = json { + if let Some(parent) = self.path.parent() { + let _ = std::fs::create_dir_all(parent); + } let _ = std::fs::write(&self.path, json); } } @@ -78,14 +78,7 @@ impl FrecencyStore { .unwrap_or_default() .as_secs(); let age_secs = now.saturating_sub(entry.last_used); - let decay = if age_secs < 3600 { - 4 - } else if age_secs < 86400 { - 2 - } else { - 1 - }; - entry.count * decay + entry.count * decay_factor(age_secs) } pub fn top_ids(&self, n: usize) -> Vec { @@ -98,14 +91,7 @@ impl FrecencyStore { .iter() .map(|(id, entry)| { let age_secs = now.saturating_sub(entry.last_used); - let decay = if age_secs < 3600 { - 4 - } else if age_secs < 86400 { - 2 - } else { - 1 - }; - (id.clone(), entry.count * decay) + (id.clone(), entry.count * decay_factor(age_secs)) }) .collect(); scored.sort_by(|a, b| b.1.cmp(&a.1)); @@ -113,6 +99,16 @@ impl FrecencyStore { } } +fn decay_factor(age_secs: u64) -> u32 { + if age_secs < 3600 { + 4 + } else if age_secs < 86400 { + 2 + } else { + 1 + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/plugins/plugin-apps/src/lib.rs b/crates/plugins/plugin-apps/src/lib.rs index 0208d4e..f86c180 100644 --- a/crates/plugins/plugin-apps/src/lib.rs +++ b/crates/plugins/plugin-apps/src/lib.rs @@ -76,7 +76,6 @@ struct CachedEntry { category: Option, icon: Option, exec: String, - on_select: Arc, } // --- Serializable cache data (no closures) --- @@ -92,24 +91,16 @@ struct CachedEntryData { } fn cache_path() -> Option { - #[cfg(test)] - return None; - #[cfg(not(test))] dirs::cache_dir().map(|d| d.join("k-launcher/apps.bin")) } -fn load_from_path(path: &Path, frecency: &Arc) -> Option> { +fn load_from_path(path: &Path) -> Option> { let data = std::fs::read(path).ok()?; let (entries_data, _): (Vec, _) = bincode::serde::decode_from_slice(&data, bincode::config::standard()).ok()?; let map = entries_data .into_iter() .map(|e| { - let store = Arc::clone(frecency); - let record_id = e.id.clone(); - let on_select: Arc = Arc::new(move || { - store.record(&record_id); - }); let cached = CachedEntry { id: e.id.clone(), name: AppName::new(e.name), @@ -117,7 +108,6 @@ fn load_from_path(path: &Path, frecency: &Arc) -> Option .entries() .into_iter() .map(|e| { - let id = format!("app-{}", e.name.as_str()); + let id = format!("app-{}:{}", e.name.as_str(), e.exec.as_str()); let keywords_lc = e.keywords.iter().map(|k| k.to_lowercase()).collect(); #[cfg(target_os = "linux")] let icon = e @@ -160,18 +150,12 @@ fn build_entries(source: &impl DesktopEntrySource, frecency: &Arc #[cfg(not(target_os = "linux"))] let icon: Option = None; let exec = e.exec.as_str().to_string(); - let store = Arc::clone(frecency); - let record_id = id.clone(); - let on_select: Arc = Arc::new(move || { - store.record(&record_id); - }); let cached = CachedEntry { id: id.clone(), keywords_lc, category: e.category, icon, exec, - on_select, name: e.name, }; (id, cached) @@ -188,16 +172,25 @@ pub struct AppsPlugin { impl AppsPlugin { pub fn new(source: impl DesktopEntrySource + 'static, frecency: Arc) -> Self { - let cached = cache_path().and_then(|p| load_from_path(&p, &frecency)); + Self::new_impl(source, frecency, cache_path()) + } + + fn new_impl( + source: impl DesktopEntrySource + 'static, + frecency: Arc, + cp: Option, + ) -> Self { + let cached = cp.as_deref().and_then(load_from_path); let entries = if let Some(from_cache) = cached { // Serve cache immediately; refresh in background. let map = Arc::new(RwLock::new(from_cache)); let entries_bg = Arc::clone(&map); let frecency_bg = Arc::clone(&frecency); + let cp_bg = cp.clone(); std::thread::spawn(move || { let fresh = build_entries(&source, &frecency_bg); - if let Some(path) = cache_path() { + if let Some(path) = cp_bg { save_to_path(&path, &fresh); } *entries_bg.write().unwrap() = fresh; @@ -206,14 +199,19 @@ impl AppsPlugin { } else { // No cache: build synchronously, then persist. let initial = build_entries(&source, &frecency); - if let Some(path) = cache_path() { - save_to_path(&path, &initial); + if let Some(path) = &cp { + save_to_path(path, &initial); } Arc::new(RwLock::new(initial)) }; Self { entries, frecency } } + + #[cfg(test)] + fn new_for_test(source: impl DesktopEntrySource + 'static, frecency: Arc) -> Self { + Self::new_impl(source, frecency, None) + } } fn initials(name_lc: &str) -> String { @@ -267,6 +265,10 @@ impl Plugin for AppsPlugin { "apps" } + fn on_selected(&self, id: &ResultId) { + self.frecency.record(id.as_str()); + } + async fn search(&self, query: &str) -> Vec { let entries = self.entries.read().unwrap(); if query.is_empty() { @@ -284,7 +286,6 @@ impl Plugin for AppsPlugin { icon: e.icon.clone(), score: Score::new(score), action: LaunchAction::SpawnProcess(e.exec.clone()), - on_select: Some(Arc::clone(&e.on_select)), }) }) .collect(); @@ -307,7 +308,6 @@ impl Plugin for AppsPlugin { icon: e.icon.clone(), score: Score::new(score), action: LaunchAction::SpawnProcess(e.exec.clone()), - on_select: Some(Arc::clone(&e.on_select)), }) }) .collect() @@ -381,7 +381,7 @@ mod tests { #[tokio::test] async fn apps_prefix_match() { - let p = AppsPlugin::new( + let p = AppsPlugin::new_for_test( MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency(), ); @@ -391,7 +391,7 @@ mod tests { #[tokio::test] async fn apps_no_match_returns_empty() { - let p = AppsPlugin::new( + let p = AppsPlugin::new_for_test( MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency(), ); @@ -400,7 +400,7 @@ mod tests { #[tokio::test] async fn apps_empty_query_no_frecency_returns_empty() { - let p = AppsPlugin::new( + let p = AppsPlugin::new_for_test( MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency(), ); @@ -414,7 +414,7 @@ mod tests { } #[test] - fn score_match_exact_beats_prefix_beats_abbrev_beats_substr() { + fn score_match_exact_beats_prefix() { let exact = score_match("firefox", "firefox"); let prefix = score_match("firefox", "fire"); let abbrev = score_match("gnu firefox", "gf"); @@ -428,7 +428,7 @@ mod tests { #[tokio::test] async fn apps_abbreviation_match() { - let p = AppsPlugin::new( + let p = AppsPlugin::new_for_test( MockSource::with(vec![("Visual Studio Code", "code")]), ephemeral_frecency(), ); @@ -440,7 +440,7 @@ mod tests { #[tokio::test] async fn apps_keyword_match() { - let p = AppsPlugin::new( + let p = AppsPlugin::new_for_test( MockSource::with_keywords(vec![("Code", "code", vec!["editor", "ide"])]), ephemeral_frecency(), ); @@ -451,7 +451,7 @@ mod tests { #[tokio::test] async fn apps_fuzzy_typo_match() { - let p = AppsPlugin::new( + let p = AppsPlugin::new_for_test( MockSource::with(vec![("Firefox", "firefox")]), ephemeral_frecency(), ); @@ -472,7 +472,7 @@ mod tests { #[tokio::test] async fn apps_category_appears_in_description() { - let p = AppsPlugin::new( + let p = AppsPlugin::new_for_test( MockSource::with_categories(vec![("Code", "code", "Text Editor")]), ephemeral_frecency(), ); @@ -483,10 +483,10 @@ mod tests { #[tokio::test] async fn apps_empty_query_returns_top_frecent() { let frecency = ephemeral_frecency(); - frecency.record("app-Code"); - frecency.record("app-Code"); - frecency.record("app-Firefox"); - let p = AppsPlugin::new( + frecency.record("app-Code:code"); + frecency.record("app-Code:code"); + frecency.record("app-Firefox:firefox"); + let p = AppsPlugin::new_for_test( MockSource::with(vec![("Firefox", "firefox"), ("Code", "code")]), frecency, ); @@ -507,8 +507,8 @@ mod tests { save_to_path(&cache_file, &entries); // Load from temp path — should contain Firefox - let loaded = load_from_path(&cache_file, &frecency).unwrap(); - assert!(loaded.contains_key("app-Firefox")); + let loaded = load_from_path(&cache_file).unwrap(); + assert!(loaded.contains_key("app-Firefox:firefox")); std::fs::remove_file(&cache_file).ok(); } diff --git a/crates/plugins/plugin-calc/src/lib.rs b/crates/plugins/plugin-calc/src/lib.rs index a7bfcf6..3e823eb 100644 --- a/crates/plugins/plugin-calc/src/lib.rs +++ b/crates/plugins/plugin-calc/src/lib.rs @@ -90,7 +90,6 @@ impl Plugin for CalcPlugin { icon: None, score: Score::new(90), action: LaunchAction::CopyToClipboard(value_str), - on_select: None, }] } _ => vec![], diff --git a/crates/plugins/plugin-cmd/src/lib.rs b/crates/plugins/plugin-cmd/src/lib.rs index 7ba56c1..50d0165 100644 --- a/crates/plugins/plugin-cmd/src/lib.rs +++ b/crates/plugins/plugin-cmd/src/lib.rs @@ -36,7 +36,6 @@ impl Plugin for CmdPlugin { icon: None, score: Score::new(95), action: LaunchAction::SpawnInTerminal(cmd.to_string()), - on_select: None, }] } } diff --git a/crates/plugins/plugin-files/src/lib.rs b/crates/plugins/plugin-files/src/lib.rs index 08aa3dc..d4eb426 100644 --- a/crates/plugins/plugin-files/src/lib.rs +++ b/crates/plugins/plugin-files/src/lib.rs @@ -71,21 +71,19 @@ impl Plugin for FilesPlugin { .unwrap_or(false) }) .take(20) - .enumerate() - .map(|(i, entry)| { + .map(|entry| { let full_path = entry.path(); let name = entry.file_name().to_string_lossy().to_string(); let is_dir = full_path.is_dir(); let title = if is_dir { format!("{name}/") } else { name }; let path_str = full_path.to_string_lossy().to_string(); SearchResult { - id: ResultId::new(format!("file-{i}")), + id: ResultId::new(&path_str), title: ResultTitle::new(title), description: Some(path_str.clone()), icon: None, score: Score::new(50), action: LaunchAction::OpenPath(path_str), - on_select: None, } }) .collect() diff --git a/crates/plugins/plugin-url/src/main.rs b/crates/plugins/plugin-url/src/main.rs index 119d9e0..002bdc4 100644 --- a/crates/plugins/plugin-url/src/main.rs +++ b/crates/plugins/plugin-url/src/main.rs @@ -10,7 +10,7 @@ struct Query { #[derive(Serialize)] struct Action { r#type: &'static str, - cmd: String, + path: String, } #[derive(Serialize)] @@ -45,8 +45,8 @@ fn search(query: &str) -> Vec { description: url.clone(), score: 95, action: Action { - r#type: "SpawnProcess", - cmd: format!("xdg-open {url}"), + r#type: "OpenPath", + path: url.clone(), }, }] } @@ -112,7 +112,7 @@ mod tests { fn search_returns_result() { let results = search("https://example.com"); assert_eq!(results.len(), 1); - assert_eq!(results[0].action.cmd, "xdg-open https://example.com"); + assert_eq!(results[0].action.path, "https://example.com"); } #[test] @@ -124,7 +124,7 @@ mod tests { fn result_serializes() { let results = search("https://example.com"); let json = serde_json::to_string(&results).unwrap(); - assert!(json.contains("SpawnProcess")); - assert!(json.contains("xdg-open")); + assert!(json.contains("OpenPath")); + assert!(json.contains("https://example.com")); } }