use crate::embedded::{EmbeddedPlugin, EMBEDDED};
use crate::manifest::PluginManifest;
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum PluginSource {
Disk { base_dir: PathBuf },
Embedded { filter_lf: &'static str },
}
impl PluginSource {
pub fn display_path(&self, category: &str, name: &str) -> PathBuf {
match self {
PluginSource::Disk { base_dir } => base_dir.clone(),
PluginSource::Embedded { .. } => {
PathBuf::from(format!("<embedded>/{category}/{name}"))
}
}
}
pub fn is_embedded(&self) -> bool {
matches!(self, PluginSource::Embedded { .. })
}
}
#[derive(Debug)]
pub struct DiscoveredPlugin {
pub manifest: PluginManifest,
pub category: String,
pub source: PluginSource,
}
impl DiscoveredPlugin {
pub fn base_dir(&self) -> PathBuf {
self.source.display_path(&self.category, &self.manifest.plugin.name)
}
pub fn is_embedded(&self) -> bool {
self.source.is_embedded()
}
}
pub fn discover_plugins(plugin_dir: &Path) -> Vec<DiscoveredPlugin> {
let mut plugins = Vec::new();
scan_plugin_dir(plugin_dir, &mut plugins);
let taken: std::collections::HashSet<String> = plugins
.iter()
.map(|p| p.manifest.plugin.name.clone())
.collect();
for emb in EMBEDDED {
if taken.contains(emb.name) {
continue;
}
if let Some(plugin) = build_embedded(emb) {
plugins.push(plugin);
}
}
plugins
}
fn build_embedded(emb: &'static EmbeddedPlugin) -> Option<DiscoveredPlugin> {
let manifest = match PluginManifest::parse(emb.manifest) {
Ok(m) => m,
Err(e) => {
eprintln!(
"[lowfat] internal: embedded plugin {} has invalid manifest: {e}",
emb.name
);
return None;
}
};
Some(DiscoveredPlugin {
manifest,
category: emb.category.into(),
source: PluginSource::Embedded {
filter_lf: emb.filter_lf,
},
})
}
fn scan_plugin_dir(dir: &Path, plugins: &mut Vec<DiscoveredPlugin>) {
let entries = match fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for category_entry in entries.flatten() {
let category_path = category_entry.path();
if !category_path.is_dir() {
continue;
}
let category = category_entry
.file_name()
.to_string_lossy()
.to_string();
let plugin_entries = match fs::read_dir(&category_path) {
Ok(e) => e,
Err(_) => continue,
};
for plugin_entry in plugin_entries.flatten() {
let plugin_path = plugin_entry.path();
let manifest_path = if plugin_path.join("lowfat.toml").is_file() {
plugin_path.join("lowfat.toml")
} else if plugin_path.join("init.toml").is_file() {
plugin_path.join("init.toml")
} else {
continue;
};
let content = match fs::read_to_string(&manifest_path) {
Ok(c) => c,
Err(_) => continue,
};
let manifest = match PluginManifest::parse(&content) {
Ok(m) => m,
Err(e) => {
eprintln!(
"[lowfat] warning: invalid manifest at {}: {}",
manifest_path.display(),
e
);
continue;
}
};
plugins.push(DiscoveredPlugin {
manifest,
category: category.clone(),
source: PluginSource::Disk { base_dir: plugin_path },
});
break;
}
}
}
pub fn resolve_plugins(plugins: &[DiscoveredPlugin]) -> HashMap<String, usize> {
let mut map = HashMap::new();
for (idx, plugin) in plugins.iter().enumerate() {
for cmd in &plugin.manifest.plugin.commands {
map.insert(cmd.clone(), idx);
}
}
map
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_plugins_discover_and_parse() {
let found = discover_plugins(Path::new("/nonexistent-lowfat-test-dir"));
let embedded: Vec<_> = found.iter().filter(|p| p.is_embedded()).collect();
assert_eq!(embedded.len(), EMBEDDED.len(), "all embedded plugins discovered");
for p in &embedded {
if let PluginSource::Embedded { filter_lf } = &p.source {
assert!(
lowfat_core::lf::parse(filter_lf).is_ok(),
"{}: embedded filter.lf failed to parse",
p.manifest.plugin.name
);
}
}
}
}