Files
owlry/crates/owlry-core/src/providers/command.rs
vikingowl 7275fcab35 fix: implement all 24 FIX_PLAN issues across 6 phases
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
2026-04-08 16:43:52 +02:00

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
}
}