lowfat_plugin/
discovery.rs1use crate::embedded::{EmbeddedPlugin, EMBEDDED};
2use crate::manifest::PluginManifest;
3use std::collections::HashMap;
4use std::fs;
5use std::path::{Path, PathBuf};
6
7#[derive(Debug)]
11pub enum PluginSource {
12 Disk { base_dir: PathBuf },
13 Embedded { filter_lf: &'static str },
14}
15
16impl PluginSource {
17 pub fn display_path(&self, category: &str, name: &str) -> PathBuf {
21 match self {
22 PluginSource::Disk { base_dir } => base_dir.clone(),
23 PluginSource::Embedded { .. } => {
24 PathBuf::from(format!("<embedded>/{category}/{name}"))
25 }
26 }
27 }
28
29 pub fn is_embedded(&self) -> bool {
30 matches!(self, PluginSource::Embedded { .. })
31 }
32}
33
34#[derive(Debug)]
36pub struct DiscoveredPlugin {
37 pub manifest: PluginManifest,
38 pub category: String,
39 pub source: PluginSource,
40}
41
42impl DiscoveredPlugin {
43 pub fn base_dir(&self) -> PathBuf {
46 self.source.display_path(&self.category, &self.manifest.plugin.name)
47 }
48
49 pub fn is_embedded(&self) -> bool {
50 self.source.is_embedded()
51 }
52}
53
54pub fn discover_plugins(plugin_dir: &Path) -> Vec<DiscoveredPlugin> {
61 let mut plugins = Vec::new();
62 scan_plugin_dir(plugin_dir, &mut plugins);
63
64 let taken: std::collections::HashSet<String> = plugins
68 .iter()
69 .map(|p| p.manifest.plugin.name.clone())
70 .collect();
71 for emb in EMBEDDED {
72 if taken.contains(emb.name) {
73 continue;
74 }
75 if let Some(plugin) = build_embedded(emb) {
76 plugins.push(plugin);
77 }
78 }
79 plugins
80}
81
82fn build_embedded(emb: &'static EmbeddedPlugin) -> Option<DiscoveredPlugin> {
83 let manifest = match PluginManifest::parse(emb.manifest) {
84 Ok(m) => m,
85 Err(e) => {
86 eprintln!(
87 "[lowfat] internal: embedded plugin {} has invalid manifest: {e}",
88 emb.name
89 );
90 return None;
91 }
92 };
93 Some(DiscoveredPlugin {
94 manifest,
95 category: emb.category.into(),
96 source: PluginSource::Embedded {
97 filter_lf: emb.filter_lf,
98 },
99 })
100}
101
102fn scan_plugin_dir(dir: &Path, plugins: &mut Vec<DiscoveredPlugin>) {
103 let entries = match fs::read_dir(dir) {
104 Ok(e) => e,
105 Err(_) => return,
106 };
107
108 for category_entry in entries.flatten() {
109 let category_path = category_entry.path();
110 if !category_path.is_dir() {
111 continue;
112 }
113 let category = category_entry
114 .file_name()
115 .to_string_lossy()
116 .to_string();
117
118 let plugin_entries = match fs::read_dir(&category_path) {
119 Ok(e) => e,
120 Err(_) => continue,
121 };
122
123 for plugin_entry in plugin_entries.flatten() {
124 let plugin_path = plugin_entry.path();
125
126 let manifest_path = if plugin_path.join("lowfat.toml").is_file() {
128 plugin_path.join("lowfat.toml")
129 } else if plugin_path.join("init.toml").is_file() {
130 plugin_path.join("init.toml")
131 } else {
132 continue;
133 };
134
135 let content = match fs::read_to_string(&manifest_path) {
136 Ok(c) => c,
137 Err(_) => continue,
138 };
139
140 let manifest = match PluginManifest::parse(&content) {
141 Ok(m) => m,
142 Err(e) => {
143 eprintln!(
144 "[lowfat] warning: invalid manifest at {}: {}",
145 manifest_path.display(),
146 e
147 );
148 continue;
149 }
150 };
151
152 plugins.push(DiscoveredPlugin {
153 manifest,
154 category: category.clone(),
155 source: PluginSource::Disk { base_dir: plugin_path },
156 });
157 break;
158 }
159 }
160}
161
162pub fn resolve_plugins(plugins: &[DiscoveredPlugin]) -> HashMap<String, usize> {
165 let mut map = HashMap::new();
166 for (idx, plugin) in plugins.iter().enumerate() {
167 for cmd in &plugin.manifest.plugin.commands {
168 map.insert(cmd.clone(), idx);
169 }
170 }
171 map
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
182 fn embedded_plugins_discover_and_parse() {
183 let found = discover_plugins(Path::new("/nonexistent-lowfat-test-dir"));
185 let embedded: Vec<_> = found.iter().filter(|p| p.is_embedded()).collect();
186 assert_eq!(embedded.len(), EMBEDDED.len(), "all embedded plugins discovered");
187
188 for p in &embedded {
189 if let PluginSource::Embedded { filter_lf } = &p.source {
190 assert!(
191 lowfat_core::lf::parse(filter_lf).is_ok(),
192 "{}: embedded filter.lf failed to parse",
193 p.manifest.plugin.name
194 );
195 }
196 }
197 }
198}