feat(plugin): enable/disable plugins from the Plugin Manager

The manager was a read-only inventory. Each plugin now has an
Enable/Disable toggle: a disabled plugin keeps its manifest listed but
drops its ribbon tab and command dispatch. The disabled-id set persists
in settings.txt (disabled_plugins=) and the ribbon rebuilds on toggle.

Part of the #100 extensibility epic.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-17 10:15:38 +03:00
1 parent 195415af84
commit 844069b4ef
9 files changed
+157 -23

No files matched your search

+1 -1
View File
@@ -8,6 +8,6 @@ pub mod host;
pub mod manifest;
pub mod registry;
pub use registry::all_ribbon_modules;
pub use registry::{all_ribbon_modules, ribbon_modules_enabled};
pub(crate) use host::BuiltinPlugin;
pub(crate) use registry::{installed_manifests, try_dispatch};
+32 -2
View File
@@ -28,11 +28,20 @@ pub fn installed_manifests() -> Vec<&'static PluginManifest> {
manifests
}
/// Core ribbon tabs plus add-on tabs (sorted by `manifest.ribbon_order`).
/// Core ribbon tabs plus *every* add-on tab (sorted by `manifest.ribbon_order`).
pub fn all_ribbon_modules() -> Vec<Box<dyn CadModule>> {
ribbon_modules_enabled(&rustc_hash::FxHashSet::default())
}
/// Core ribbon tabs plus add-on tabs whose plugin id is **not** in `disabled`
/// (sorted by `manifest.ribbon_order`). Used by the Plugin Manager toggle.
pub fn ribbon_modules_enabled(
disabled: &rustc_hash::FxHashSet<String>,
) -> Vec<Box<dyn CadModule>> {
let mut core = core_registry::all_modules();
let mut addons: Vec<(i32, Box<dyn CadModule>)> = all_plugins()
.into_iter()
.filter(|p| !disabled.contains(p.manifest().id))
.map(|p| (p.manifest().ribbon_order, p.ribbon()))
.collect();
addons.sort_by_key(|(order, _)| *order);
@@ -40,10 +49,15 @@ pub fn all_ribbon_modules() -> Vec<Box<dyn CadModule>> {
core
}
/// Try each plugin until one handles `cmd`. Returns true if handled.
/// Try each *enabled* plugin until one handles `cmd`. Returns true if handled.
/// Disabled plugins (toggled off in the Plugin Manager) are skipped.
pub(crate) fn try_dispatch(app: &mut OpenCADStudio, tab: usize, cmd: &str) -> bool {
let disabled = app.disabled_plugin_ids();
let mut host = HostSession::new(app, tab);
for plugin in all_plugins() {
if disabled.contains(plugin.manifest().id) {
continue;
}
if plugin.dispatch(&mut host, cmd) {
return true;
}
@@ -98,6 +112,22 @@ mod tests {
assert_eq!(titles.len(), core.len() + all_plugins().len());
}
#[test]
fn disabled_plugin_drops_its_ribbon_tab() {
let mut disabled = rustc_hash::FxHashSet::default();
disabled.insert("opencad.demo_plugin".to_string());
let titles: Vec<&str> = ribbon_modules_enabled(&disabled)
.iter()
.map(|m| m.title())
.collect();
assert!(
!titles.contains(&"Demo Plugin"),
"disabled plugin still present: {titles:?}"
);
// Only the add-on tab is dropped; core tabs stay.
assert_eq!(titles.len(), core_registry::all_modules().len());
}
#[test]
fn try_dispatch_routes_demo_command() {
let mut app = OpenCADStudio::new_for_test();