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)
This commit is contained in:
2026-03-18 13:45:48 +01:00
parent 38860762c0
commit ff9b2b5712
18 changed files with 189 additions and 133 deletions

View File

@@ -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<iced::widget::Id> =
std::sync::LazyLock::new(|| iced::widget::Id::new("search"));
@@ -63,6 +62,7 @@ pub enum Message {
ResultsReady(u64, Arc<Vec<SearchResult>>),
KeyPressed(KeyEvent),
EngineReady(EngineHandle),
EngineInitFailed(String),
}
fn update(state: &mut KLauncherApp, message: Message) -> Task<Message> {
@@ -88,6 +88,10 @@ fn update(state: &mut KLauncherApp, message: Message) -> Task<Message> {
}
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<Message> {
}
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()