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

@@ -3,6 +3,29 @@ use std::process::{Command, Stdio};
use k_launcher_kernel::{AppLauncher, LaunchAction};
fn shell_split(cmd: &str) -> Vec<String> {
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<String>) {
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"]);
}
}