forked from 1b-agent/opencadstudio-upstream
feat(plugin): marketplace — install plugins from a curated registry
Add a Plugin Manager marketplace that installs external add-ons from a GitHub repo's Releases. A curated registry (plugins/registry.json in this repo, extended by PR) lists discoverable plugins; the host fetches it, shows each entry with a release dropdown, and on install downloads the asset matching the user's platform plus plugin.toml into the plugins folder (API-version checked first). Users can also link arbitrary owner/repo manually. Linked repos persist in settings. Desktop only (ureq + serde_json); the install path reuses the phase-2 loader. Part of the #100 extensibility epic (phase 2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
29c71be101
commit
020a3a4c84
12 files changed
+599
-5
No files matched your search
@@ -303,6 +303,18 @@ pub(super) struct OpenCADStudio {
|
||||
/// Ids of external packages actually loaded this session (a subset of
|
||||
/// `external_plugins` — compatible, with a library, dlopen'd at startup).
|
||||
loaded_plugin_ids: rustc_hash::FxHashSet<String>,
|
||||
/// Curated plugin registry fetched from the OpenCADStudio repo.
|
||||
plugin_registry: Vec<crate::plugin::external::RegistryEntry>,
|
||||
/// User-linked plugin source repos (`owner/repo`) beyond the curated list.
|
||||
plugin_repos: Vec<String>,
|
||||
/// Add-repository text field in the Plugin Manager.
|
||||
plugin_repo_input: String,
|
||||
/// Installable release tags fetched per linked repo (for the dropdown).
|
||||
repo_release_tags: rustc_hash::FxHashMap<String, Vec<String>>,
|
||||
/// The release tag currently selected per linked repo.
|
||||
repo_selected_tag: rustc_hash::FxHashMap<String, String>,
|
||||
/// Last marketplace status / error line shown in the Plugin Manager.
|
||||
marketplace_status: String,
|
||||
/// PDSIZE text buffer for the Point Style (DDPTYPE) dialog.
|
||||
point_size_buf: String,
|
||||
/// Point Style size mode: `true` = relative to screen, `false` = absolute.
|
||||
@@ -1044,6 +1056,23 @@ pub enum Message {
|
||||
PluginManagerClose,
|
||||
/// Enable (`true`) or disable (`false`) the plugin with this id.
|
||||
SetPluginEnabled(String, bool),
|
||||
// ── Plugin marketplace (install from a linked repo's releases) ─────────
|
||||
/// Edit the add-repository text field.
|
||||
PluginRepoInput(String),
|
||||
/// Link the repository currently in the text field.
|
||||
PluginRepoAdd,
|
||||
/// Unlink a repository.
|
||||
PluginRepoRemove(String),
|
||||
/// The curated registry was fetched.
|
||||
PluginRegistryFetched(Result<Vec<crate::plugin::external::RegistryEntry>, String>),
|
||||
/// Installable release tags fetched for `owner/repo`.
|
||||
PluginReleasesFetched(String, Result<Vec<String>, String>),
|
||||
/// Choose a release tag for a repo (`repo`, `tag`).
|
||||
PluginReleaseSelect(String, String),
|
||||
/// Install the selected release of `owner/repo`.
|
||||
PluginInstall(String),
|
||||
/// Result of an install: the plugin id, or an error message.
|
||||
PluginInstalled(Result<String, String>),
|
||||
// ── Point Style (DDPTYPE) dialog ──────────────────────────────────────
|
||||
/// Set the full PDMODE value from a glyph-grid cell.
|
||||
PointStyleSetMode(i16),
|
||||
@@ -1469,6 +1498,12 @@ impl OpenCADStudio {
|
||||
disabled_plugins: rustc_hash::FxHashSet::default(),
|
||||
external_plugins: Vec::new(),
|
||||
loaded_plugin_ids: rustc_hash::FxHashSet::default(),
|
||||
plugin_registry: Vec::new(),
|
||||
plugin_repos: Vec::new(),
|
||||
plugin_repo_input: String::new(),
|
||||
repo_release_tags: rustc_hash::FxHashMap::default(),
|
||||
repo_selected_tag: rustc_hash::FxHashMap::default(),
|
||||
marketplace_status: String::new(),
|
||||
point_size_buf: String::new(),
|
||||
point_size_relative: true,
|
||||
default_assoc_prompted: false,
|
||||
|
||||
+14
-1
@@ -78,6 +78,9 @@ pub struct UserSettings {
|
||||
/// plugins keep their manifest listed but drop their ribbon tab and command
|
||||
/// dispatch.
|
||||
pub disabled_plugins: Vec<String>,
|
||||
/// Linked plugin source repositories (`owner/repo`) the marketplace installs
|
||||
/// from.
|
||||
pub plugin_repos: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for UserSettings {
|
||||
@@ -101,6 +104,7 @@ impl Default for UserSettings {
|
||||
],
|
||||
default_assoc_prompted: false,
|
||||
disabled_plugins: Vec::new(),
|
||||
plugin_repos: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +147,14 @@ impl UserSettings {
|
||||
.map(|t| t.to_string())
|
||||
.collect();
|
||||
}
|
||||
"plugin_repos" => {
|
||||
s.plugin_repos = val
|
||||
.split(',')
|
||||
.map(|t| t.trim())
|
||||
.filter(|t| !t.is_empty())
|
||||
.map(|t| t.to_string())
|
||||
.collect();
|
||||
}
|
||||
"snap_modes" => {
|
||||
let modes: Vec<SnapType> =
|
||||
val.split(',').filter_map(|t| snap_from_id(t.trim())).collect();
|
||||
@@ -168,7 +180,7 @@ impl UserSettings {
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let body = format!(
|
||||
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\ngrid={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\n",
|
||||
"dyn={}\northo={}\npolar={}\npolar_increment_deg={}\ngrid={}\nosnap={}\notrack={}\ndefault_assoc_prompted={}\nsnap_modes={}\ndisabled_plugins={}\nplugin_repos={}\n",
|
||||
b(self.dyn_input),
|
||||
b(self.ortho),
|
||||
b(self.polar),
|
||||
@@ -179,6 +191,7 @@ impl UserSettings {
|
||||
b(self.default_assoc_prompted),
|
||||
modes,
|
||||
self.disabled_plugins.join(","),
|
||||
self.plugin_repos.join(","),
|
||||
);
|
||||
let _ = std::fs::write(path, body);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,7 @@ impl OpenCADStudio {
|
||||
v.sort();
|
||||
v
|
||||
},
|
||||
plugin_repos: self.plugin_repos.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +162,7 @@ impl OpenCADStudio {
|
||||
self.snapper.enabled = s.snap_modes.iter().copied().collect();
|
||||
self.default_assoc_prompted = s.default_assoc_prompted;
|
||||
self.disabled_plugins = s.disabled_plugins.iter().cloned().collect();
|
||||
self.plugin_repos = s.plugin_repos.clone();
|
||||
self.rebuild_ribbon_modules();
|
||||
}
|
||||
|
||||
@@ -178,6 +180,58 @@ impl OpenCADStudio {
|
||||
self.disabled_plugins.clone()
|
||||
}
|
||||
|
||||
/// Background task: fetch the curated plugin registry.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn fetch_registry_task(&self) -> Task<Message> {
|
||||
Task::perform(
|
||||
async { crate::plugin::marketplace::fetch_registry() },
|
||||
Message::PluginRegistryFetched,
|
||||
)
|
||||
}
|
||||
|
||||
/// Background task: fetch `owner/repo`'s installable release tags.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn fetch_releases_task(&self, repo: String) -> Task<Message> {
|
||||
let label = repo.clone();
|
||||
Task::perform(
|
||||
async move {
|
||||
crate::plugin::marketplace::fetch_releases(&repo).map(|rs| {
|
||||
rs.into_iter()
|
||||
.filter(|r| r.installable())
|
||||
.map(|r| r.tag)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
},
|
||||
move |res| Message::PluginReleasesFetched(label, res),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn fetch_releases_task(&self, _repo: String) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Background task: download and install the `tag` release of `owner/repo`.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn install_task(&self, repo: String, tag: String) -> Task<Message> {
|
||||
Task::perform(
|
||||
async move {
|
||||
let releases = crate::plugin::marketplace::fetch_releases(&repo)?;
|
||||
let rel = releases
|
||||
.into_iter()
|
||||
.find(|r| r.tag == tag)
|
||||
.ok_or_else(|| format!("release {tag} not found"))?;
|
||||
crate::plugin::marketplace::install(&rel)
|
||||
},
|
||||
Message::PluginInstalled,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn install_task(&self, _repo: String, _tag: String) -> Task<Message> {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Write PDSIZE from the dialog buffer with the current relative/absolute
|
||||
/// sign. A relative size is stored negative; absolute positive. Switching to
|
||||
/// absolute with an empty/zero size seeds a positive value from the current
|
||||
@@ -5530,6 +5584,19 @@ impl OpenCADStudio {
|
||||
// opens so newly dropped-in packages show up.
|
||||
self.external_plugins = crate::plugin::external::discover();
|
||||
self.active_modal = Some(super::ModalKind::PluginManager);
|
||||
// Fetch the curated registry and release lists for linked repos.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let mut tasks = vec![self.fetch_registry_task()];
|
||||
tasks.extend(
|
||||
self.plugin_repos
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|r| self.fetch_releases_task(r)),
|
||||
);
|
||||
return Task::batch(tasks);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginManagerClose => {
|
||||
@@ -5546,6 +5613,92 @@ impl OpenCADStudio {
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginRepoInput(s) => {
|
||||
self.plugin_repo_input = s;
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginRepoAdd => {
|
||||
let repo = self
|
||||
.plugin_repo_input
|
||||
.trim()
|
||||
.trim_start_matches("https://github.com/")
|
||||
.trim_end_matches('/')
|
||||
.to_string();
|
||||
if repo.is_empty() || self.plugin_repos.contains(&repo) {
|
||||
return Task::none();
|
||||
}
|
||||
self.plugin_repos.push(repo.clone());
|
||||
self.plugin_repo_input.clear();
|
||||
self.persist_settings_if_changed();
|
||||
self.marketplace_status = format!("Fetching releases for {repo}…");
|
||||
self.fetch_releases_task(repo)
|
||||
}
|
||||
Message::PluginRepoRemove(repo) => {
|
||||
self.plugin_repos.retain(|r| r != &repo);
|
||||
self.repo_release_tags.remove(&repo);
|
||||
self.repo_selected_tag.remove(&repo);
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginRegistryFetched(Ok(entries)) => {
|
||||
// Fetch releases for every curated repo so the dropdowns fill in.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let tasks: Vec<_> = entries
|
||||
.iter()
|
||||
.map(|e| self.fetch_releases_task(e.repo.clone()))
|
||||
.collect();
|
||||
self.plugin_registry = entries;
|
||||
return Task::batch(tasks);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
{
|
||||
self.plugin_registry = entries;
|
||||
Task::none()
|
||||
}
|
||||
}
|
||||
Message::PluginRegistryFetched(Err(e)) => {
|
||||
self.marketplace_status = format!("Registry: {e}");
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginReleasesFetched(repo, Ok(tags)) => {
|
||||
if let Some(first) = tags.first() {
|
||||
self.repo_selected_tag
|
||||
.entry(repo.clone())
|
||||
.or_insert_with(|| first.clone());
|
||||
}
|
||||
self.marketplace_status =
|
||||
format!("{repo}: {} installable release(s)", tags.len());
|
||||
self.repo_release_tags.insert(repo, tags);
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginReleasesFetched(repo, Err(e)) => {
|
||||
self.marketplace_status = format!("{repo}: {e}");
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginReleaseSelect(repo, tag) => {
|
||||
self.repo_selected_tag.insert(repo, tag);
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginInstall(repo) => {
|
||||
let Some(tag) = self.repo_selected_tag.get(&repo).cloned() else {
|
||||
return Task::none();
|
||||
};
|
||||
self.marketplace_status = format!("Installing {repo} {tag}…");
|
||||
self.install_task(repo, tag)
|
||||
}
|
||||
Message::PluginInstalled(Ok(id)) => {
|
||||
self.marketplace_status = format!("Installed '{id}'. Restart to load it.");
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
self.external_plugins = crate::plugin::external::discover();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::PluginInstalled(Err(e)) => {
|
||||
self.marketplace_status = format!("Install failed: {e}");
|
||||
Task::none()
|
||||
}
|
||||
Message::PointStyleSetMode(mode) => {
|
||||
self.set_point_mode_bits(!0, mode);
|
||||
Task::none()
|
||||
|
||||
@@ -1126,6 +1126,14 @@ impl OpenCADStudio {
|
||||
&self.disabled_plugins,
|
||||
&self.external_plugins,
|
||||
&self.loaded_plugin_ids,
|
||||
crate::ui::plugin_manager::MarketView {
|
||||
registry: &self.plugin_registry,
|
||||
input: &self.plugin_repo_input,
|
||||
repos: &self.plugin_repos,
|
||||
release_tags: &self.repo_release_tags,
|
||||
selected_tag: &self.repo_selected_tag,
|
||||
status: &self.marketplace_status,
|
||||
},
|
||||
),
|
||||
520,
|
||||
460,
|
||||
|
||||
Reference in new issue
Block a user