Phase 1 — Critical Safety: - #11: bounded IPC reads via read_bounded_line (server + client) - #13: sound Send+Sync via Arc<Mutex<RuntimeHandle>>; remove unsafe impl Sync - #10: ItemSource enum (Core/NativePlugin/ScriptPlugin) on LaunchItem; script plugin allowlist guard in launch_item() Phase 2 — Config System Overhaul: - #6: remove dead enabled_plugins field - #1: replace #[serde(flatten)] with explicit Config::plugin_config - #4: Server.config Arc<RwLock<Config>>; ConfigProvider shares same Arc - #2/#3: atomic config save (temp+rename); TOCTOU fixed — write lock held across mutation and save() in config_editor - #23: fs2 lock_exclusive() on .lock sidecar file in Config::save() - #16: SIGHUP handler reloads config; ExecReload in systemd service Phase 3 — Plugin Architecture: - #7: HostAPI v4 with get_config_string/int/bool; PLUGIN_CONFIG OnceLock in native_loader, set_shared_config() called from Server::bind() - #5: PluginEntry + Request::PluginList + Response::PluginList; plugin_registry in ProviderManager tracks active and suppressed native plugins; cmd_list_installed shows both script and native plugins - #9: suppressed native plugin log level info! → warn! - #8: ProviderType doc glossary; plugins/mod.rs terminology table Phase 4 — Data Integrity: - #12: all into_inner() in server.rs + providers/mod.rs → explicit Response::Error; watcher exits on poisoned lock - #14: FrecencyStore::prune() (180-day age + 5000-entry cap) called on load - #17: empty command guard in launch_item(); warn in lua_provider - #24: 5-min periodic frecency save thread; SIGTERM/SIGINT saves frecency before exit (replaces ctrlc handler) Phase 5 — UI & UX: - #19: provider_meta.rs ProviderMeta + meta_for(); three match blocks collapsed - #18: desktop file dedup via seen_basenames HashSet in ApplicationProvider - #20: search_filtered gains tag_filter param; non-frecency path now filters - #15: widget refresh 5s→10s; skip when user is typing Phase 6 — Hardening: - #22: catch_unwind removed from reload_runtimes(); direct drop() - #21: AtomicUsize + RAII ConnectionGuard; MAX_CONNECTIONS = 16 Deps: add fs2 = "0.4"; remove ctrlc and toml_edit from owlry-core
110 lines
2.9 KiB
Rust
110 lines
2.9 KiB
Rust
use super::{ItemSource, LaunchItem, Provider, ProviderType};
|
|
use log::debug;
|
|
use std::collections::HashSet;
|
|
use std::os::unix::fs::PermissionsExt;
|
|
use std::path::PathBuf;
|
|
|
|
#[derive(Default)]
|
|
pub struct CommandProvider {
|
|
items: Vec<LaunchItem>,
|
|
}
|
|
|
|
impl CommandProvider {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
fn get_path_dirs() -> Vec<PathBuf> {
|
|
std::env::var("PATH")
|
|
.unwrap_or_default()
|
|
.split(':')
|
|
.map(PathBuf::from)
|
|
.filter(|p| p.exists())
|
|
.collect()
|
|
}
|
|
|
|
fn is_executable(path: &std::path::Path) -> bool {
|
|
if let Ok(metadata) = path.metadata() {
|
|
let permissions = metadata.permissions();
|
|
permissions.mode() & 0o111 != 0
|
|
} else {
|
|
false
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Provider for CommandProvider {
|
|
fn name(&self) -> &str {
|
|
"Commands"
|
|
}
|
|
|
|
fn provider_type(&self) -> ProviderType {
|
|
ProviderType::Command
|
|
}
|
|
|
|
fn refresh(&mut self) {
|
|
self.items.clear();
|
|
|
|
let dirs = Self::get_path_dirs();
|
|
let mut seen_names: HashSet<String> = HashSet::new();
|
|
|
|
debug!("Scanning PATH directories for commands");
|
|
|
|
for dir in dirs {
|
|
let entries = match std::fs::read_dir(&dir) {
|
|
Ok(e) => e,
|
|
Err(_) => continue,
|
|
};
|
|
|
|
for entry in entries.filter_map(Result::ok) {
|
|
let path = entry.path();
|
|
|
|
// Skip directories and non-executable files
|
|
if path.is_dir() || !Self::is_executable(&path) {
|
|
continue;
|
|
}
|
|
|
|
let name = match path.file_name() {
|
|
Some(n) => n.to_string_lossy().to_string(),
|
|
None => continue,
|
|
};
|
|
|
|
// Skip duplicates (first one in PATH wins)
|
|
if seen_names.contains(&name) {
|
|
continue;
|
|
}
|
|
seen_names.insert(name.clone());
|
|
|
|
// Skip hidden files
|
|
if name.starts_with('.') {
|
|
continue;
|
|
}
|
|
|
|
let item = LaunchItem {
|
|
id: path.to_string_lossy().to_string(),
|
|
name: name.clone(),
|
|
description: Some(format!("Run {}", path.display())),
|
|
icon: Some("utilities-terminal".to_string()),
|
|
provider: ProviderType::Command,
|
|
command: name,
|
|
terminal: false,
|
|
tags: Vec::new(),
|
|
source: ItemSource::Core,
|
|
};
|
|
|
|
self.items.push(item);
|
|
}
|
|
}
|
|
|
|
debug!("Found {} commands in PATH", self.items.len());
|
|
|
|
// Sort alphabetically
|
|
self.items
|
|
.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
|
}
|
|
|
|
fn items(&self) -> &[LaunchItem] {
|
|
&self.items
|
|
}
|
|
}
|