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

@@ -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<String> {
@@ -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::*;