pinakes-core: expose required_endpoints alongside UI pages in plugin manager

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I32c95a03f106db8fef7eedd0362756a46a6a6964
This commit is contained in:
raf 2026-03-11 17:22:52 +03:00
commit 15b005cef0
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 119 additions and 13 deletions

View file

@ -602,7 +602,8 @@ impl PluginManager {
/// List all UI pages provided by loaded plugins. /// List all UI pages provided by loaded plugins.
/// ///
/// Returns a vector of `(plugin_id, page)` tuples for all enabled plugins /// Returns a vector of `(plugin_id, page)` tuples for all enabled plugins
/// that provide pages in their manifests. /// that provide pages in their manifests. Both inline and file-referenced
/// page entries are resolved.
pub async fn list_ui_pages( pub async fn list_ui_pages(
&self, &self,
) -> Vec<(String, pinakes_plugin_api::UiPage)> { ) -> Vec<(String, pinakes_plugin_api::UiPage)> {
@ -612,23 +613,126 @@ impl PluginManager {
if !plugin.enabled { if !plugin.enabled {
continue; continue;
} }
for entry in &plugin.manifest.ui.pages { let plugin_dir = plugin
let page = match entry { .manifest_path
pinakes_plugin_api::manifest::UiPageEntry::Inline(page) => { .as_ref()
(**page).clone() .and_then(|p| p.parent())
}, .map(std::path::Path::to_path_buf);
pinakes_plugin_api::manifest::UiPageEntry::File { .. } => { let Some(plugin_dir) = plugin_dir else {
// File-referenced pages require a base path to resolve; // No manifest path; serve only inline pages.
// skip them here as they should have been loaded at startup. for entry in &plugin.manifest.ui.pages {
continue; if let pinakes_plugin_api::manifest::UiPageEntry::Inline(page) = entry
}, {
}; pages.push((plugin.id.clone(), (**page).clone()));
pages.push((plugin.id.clone(), page)); }
}
continue;
};
match plugin.manifest.load_ui_pages(&plugin_dir) {
Ok(loaded) => {
for page in loaded {
pages.push((plugin.id.clone(), page));
}
},
Err(e) => {
tracing::warn!(
"Failed to load UI pages for plugin '{}': {e}",
plugin.id
);
},
} }
} }
pages pages
} }
/// List all UI pages provided by loaded plugins, including each plugin's
/// declared endpoint allowlist.
///
/// Returns a vector of `(plugin_id, page, allowed_endpoints)` tuples. The
/// `allowed_endpoints` list mirrors the `required_endpoints` field from the
/// plugin manifest's `[ui]` section.
pub async fn list_ui_pages_with_endpoints(
&self,
) -> Vec<(String, pinakes_plugin_api::UiPage, Vec<String>)> {
let registry = self.registry.read().await;
let mut pages = Vec::new();
for plugin in registry.list_all() {
if !plugin.enabled {
continue;
}
let allowed = plugin.manifest.ui.required_endpoints.clone();
let plugin_dir = plugin
.manifest_path
.as_ref()
.and_then(|p| p.parent())
.map(std::path::Path::to_path_buf);
let Some(plugin_dir) = plugin_dir else {
for entry in &plugin.manifest.ui.pages {
if let pinakes_plugin_api::manifest::UiPageEntry::Inline(page) = entry
{
pages.push((plugin.id.clone(), (**page).clone(), allowed.clone()));
}
}
continue;
};
match plugin.manifest.load_ui_pages(&plugin_dir) {
Ok(loaded) => {
for page in loaded {
pages.push((plugin.id.clone(), page, allowed.clone()));
}
},
Err(e) => {
tracing::warn!(
"Failed to load UI pages for plugin '{}': {e}",
plugin.id
);
},
}
}
pages
}
/// Collect CSS custom property overrides declared by all enabled plugins.
///
/// When multiple plugins declare the same property name, later-loaded plugins
/// overwrite earlier ones. Returns an empty map if no plugins are loaded or
/// none declare theme extensions.
pub async fn list_ui_theme_extensions(
&self,
) -> std::collections::HashMap<String, String> {
let registry = self.registry.read().await;
let mut merged = std::collections::HashMap::new();
for plugin in registry.list_all() {
if !plugin.enabled {
continue;
}
for (k, v) in &plugin.manifest.ui.theme_extensions {
merged.insert(k.clone(), v.clone());
}
}
merged
}
/// List all UI widgets provided by loaded plugins.
///
/// Returns a vector of `(plugin_id, widget)` tuples for all enabled plugins
/// that provide widgets in their manifests.
pub async fn list_ui_widgets(
&self,
) -> Vec<(String, pinakes_plugin_api::UiWidget)> {
let registry = self.registry.read().await;
let mut widgets = Vec::new();
for plugin in registry.list_all() {
if !plugin.enabled {
continue;
}
for widget in &plugin.manifest.ui.widgets {
widgets.push((plugin.id.clone(), widget.clone()));
}
}
widgets
}
/// Check if a plugin is loaded and enabled /// Check if a plugin is loaded and enabled
pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool { pub async fn is_plugin_enabled(&self, plugin_id: &str) -> bool {
let registry = self.registry.read().await; let registry = self.registry.read().await;
@ -746,6 +850,7 @@ mod tests {
}, },
capabilities: Default::default(), capabilities: Default::default(),
config: Default::default(), config: Default::default(),
ui: Default::default(),
} }
} }

View file

@ -182,6 +182,7 @@ mod tests {
}, },
capabilities: ManifestCapabilities::default(), capabilities: ManifestCapabilities::default(),
config: HashMap::new(), config: HashMap::new(),
ui: Default::default(),
}; };
RegisteredPlugin { RegisteredPlugin {