cad-editor/src/plugin/registry.rs
2026-08-13 09:49:57 +02:00

101 lines
3.7 KiB
Rust

// Plugin registry — external (dynamically-loaded) plugins only. OpenCADStudio
// ships no built-in add-ons; every plugin is a cdylib loaded from the plugins
// folder at startup (see `external`) and the marketplace installs them there.
use crate::app::OpenCADStudio;
use crate::modules::{registry as core_registry, CadModule};
/// Core ribbon tabs plus every loaded external add-on tab.
pub fn all_ribbon_modules() -> Vec<Box<dyn CadModule>> {
ribbon_modules_enabled(&rustc_hash::FxHashSet::default())
}
/// Core ribbon tabs plus the tabs of loaded external plugins whose id is **not**
/// in `disabled` (sorted by `manifest.ribbon_order`).
pub fn ribbon_modules_enabled(
disabled: &rustc_hash::FxHashSet<String>,
) -> Vec<Box<dyn CadModule>> {
#[cfg_attr(target_arch = "wasm32", allow(unused_mut))]
let mut core = core_registry::all_modules();
// Dynamically-loaded external plugins contribute tabs via the crate manager.
#[cfg(not(target_arch = "wasm32"))]
{
crate::plugin::external::with_manager(|manager| {
let addons = manager.ribbon_modules(|id| disabled.contains(id));
core.extend(addons.into_iter().map(|(_, module)| {
Box::new(module) as Box<dyn CadModule>
}));
});
}
let _ = disabled;
core
}
/// Command names contributed by loaded external plugins whose id is **not** in
/// `disabled` — every ribbon tool's command id plus manifest command prefixes.
/// The host merges these into command-line autocomplete so plugin commands are
/// discoverable by typing, not only via ribbon buttons (#272).
pub fn plugin_command_names(disabled: &rustc_hash::FxHashSet<String>) -> Vec<String> {
#[cfg(not(target_arch = "wasm32"))]
{
return crate::plugin::external::with_manager(|manager| {
manager.command_names(|id| disabled.contains(id))
});
}
#[cfg(target_arch = "wasm32")]
{
let _ = disabled;
Vec::new()
}
}
/// Dispatch `cmd` to a loaded external plugin (skipping disabled ones).
/// Returns true if one handled it.
pub(crate) fn try_dispatch(app: &mut OpenCADStudio, tab: usize, cmd: &str) -> bool {
#[cfg(not(target_arch = "wasm32"))]
{
use super::host::HostSession;
let disabled = app.disabled_plugin_ids();
let result = {
let mut host = HostSession::new(app, tab);
crate::plugin::external::with_manager(|manager| {
manager.dispatch(&mut host, cmd, |id| disabled.contains(id))
})
};
for id in result.dead_plugins {
app.push_plugin_error(&format!("Plugin '{id}' process died; skipping dispatch"));
}
for (id, err) in result.errors {
app.push_plugin_error(&format!("Plugin '{id}' dispatch error: {err}"));
}
if let Some((process, command_id)) = result.started {
app.set_active_command(
tab,
Box::new(crate::app::plugin_host::PluginProcessInteractiveAdapter::new(
process,
command_id,
)),
);
}
return result.handled;
}
#[cfg(target_arch = "wasm32")]
{
let _ = (app, tab, cmd);
false
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ribbon_is_core_only_without_external_plugins() {
// No external plugins are loaded under test, so the ribbon is exactly
// the built-in core tabs.
let titles: Vec<&str> = all_ribbon_modules().iter().map(|m| m.title()).collect();
assert!(!titles.is_empty(), "expected core ribbon tabs");
assert_eq!(titles.len(), core_registry::all_modules().len());
}
}