// 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> { 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, ) -> Vec> { #[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 })); }); } 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) -> Vec { #[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()); } }