From 133d5264ea07b17453df1f7d96f19a7cd575a8be Mon Sep 17 00:00:00 2001 From: vikingowl Date: Mon, 6 Apr 2026 02:22:03 +0200 Subject: [PATCH] feat(plugins): update plugin format to new entry_point + [[providers]] style - owlry-core/manifest: add entry_point alias for entry field, add ProviderSpec struct for [[providers]] array, change default entry to main.lua - owlry-lua/manifest: add ProviderDecl struct and providers: Vec for [[providers]] support - owlry-lua/loader: fall back to manifest [[providers]] when script has no API registrations; fall back to global refresh() for manifest-declared providers - owlry-lua/api: expose call_global_refresh() that calls the top-level Lua refresh() function directly - owlry/plugin_commands: update create templates to emit new format: entry_point instead of entry, [[providers]] instead of [provides], main.rn/main.lua instead of init.rn/init.lua, Rune uses Item::new() builder pattern, Lua uses standalone refresh() function - cmd_validate: accept [[providers]] declarations as a valid provides source --- crates/owlry-core/src/plugins/manifest.rs | 30 ++++- crates/owlry-lua/src/api/mod.rs | 5 + crates/owlry-lua/src/api/provider.rs | 9 ++ crates/owlry-lua/src/loader.rs | 36 +++++- crates/owlry-lua/src/manifest.rs | 24 ++++ crates/owlry/src/plugin_commands.rs | 141 ++++++++-------------- 6 files changed, 150 insertions(+), 95 deletions(-) diff --git a/crates/owlry-core/src/plugins/manifest.rs b/crates/owlry-core/src/plugins/manifest.rs index df71e26..bcf7b96 100644 --- a/crates/owlry-core/src/plugins/manifest.rs +++ b/crates/owlry-core/src/plugins/manifest.rs @@ -10,6 +10,10 @@ use super::error::{PluginError, PluginResult}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginManifest { pub plugin: PluginInfo, + /// Provider declarations from [[providers]] sections (new-style) + #[serde(default)] + pub providers: Vec, + /// Legacy provides block (old-style) #[serde(default)] pub provides: PluginProvides, #[serde(default)] @@ -43,7 +47,7 @@ pub struct PluginInfo { #[serde(default = "default_owlry_version")] pub owlry_version: String, /// Entry point file (relative to plugin directory) - #[serde(default = "default_entry")] + #[serde(default = "default_entry", alias = "entry_point")] pub entry: String, } @@ -52,7 +56,27 @@ fn default_owlry_version() -> String { } fn default_entry() -> String { - "init.lua".to_string() + "main.lua".to_string() +} + +/// A provider declared in a [[providers]] section +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderSpec { + pub id: String, + pub name: String, + #[serde(default)] + pub prefix: Option, + #[serde(default)] + pub icon: Option, + /// "static" (default) or "dynamic" + #[serde(default = "default_provider_type", rename = "type")] + pub provider_type: String, + #[serde(default)] + pub type_id: Option, +} + +fn default_provider_type() -> String { + "static".to_string() } /// What the plugin provides @@ -278,7 +302,7 @@ version = "1.0.0" assert_eq!(manifest.plugin.id, "test-plugin"); assert_eq!(manifest.plugin.name, "Test Plugin"); assert_eq!(manifest.plugin.version, "1.0.0"); - assert_eq!(manifest.plugin.entry, "init.lua"); + assert_eq!(manifest.plugin.entry, "main.lua"); } #[test] diff --git a/crates/owlry-lua/src/api/mod.rs b/crates/owlry-lua/src/api/mod.rs index a85c3df..6efaee6 100644 --- a/crates/owlry-lua/src/api/mod.rs +++ b/crates/owlry-lua/src/api/mod.rs @@ -50,3 +50,8 @@ pub fn call_refresh(lua: &Lua, provider_name: &str) -> LuaResult pub fn call_query(lua: &Lua, provider_name: &str, query: &str) -> LuaResult> { provider::call_query(lua, provider_name, query) } + +/// Call the global `refresh()` function (for manifest-declared providers) +pub fn call_global_refresh(lua: &Lua) -> LuaResult> { + provider::call_global_refresh(lua) +} diff --git a/crates/owlry-lua/src/api/provider.rs b/crates/owlry-lua/src/api/provider.rs index d781523..3096fb1 100644 --- a/crates/owlry-lua/src/api/provider.rs +++ b/crates/owlry-lua/src/api/provider.rs @@ -76,6 +76,15 @@ fn register_provider(lua: &Lua, config: Table) -> LuaResult<()> { Ok(()) } +/// Call the top-level `refresh()` global function (for manifest-declared providers) +pub fn call_global_refresh(lua: &Lua) -> LuaResult> { + let globals = lua.globals(); + match globals.get::("refresh") { + Ok(refresh_fn) => parse_items_result(refresh_fn.call(())?), + Err(_) => Ok(Vec::new()), + } +} + /// Get all registered providers pub fn get_registrations(lua: &Lua) -> LuaResult> { // Suppress unused warning diff --git a/crates/owlry-lua/src/loader.rs b/crates/owlry-lua/src/loader.rs index 0d62491..00778ae 100644 --- a/crates/owlry-lua/src/loader.rs +++ b/crates/owlry-lua/src/loader.rs @@ -96,8 +96,28 @@ impl LoadedPlugin { .as_ref() .ok_or_else(|| "Plugin not initialized".to_string())?; - api::get_provider_registrations(lua) - .map_err(|e| format!("Failed to get registrations: {}", e)) + let mut regs = api::get_provider_registrations(lua) + .map_err(|e| format!("Failed to get registrations: {}", e))?; + + // Fall back to manifest [[providers]] declarations when the script + // doesn't call owlry.provider.register() (new-style plugins) + if regs.is_empty() { + for decl in &self.manifest.providers { + regs.push(ProviderRegistration { + name: decl.id.clone(), + display_name: decl.name.clone(), + type_id: decl.type_id.clone().unwrap_or_else(|| decl.id.clone()), + default_icon: decl + .icon + .clone() + .unwrap_or_else(|| "application-x-addon".to_string()), + prefix: decl.prefix.clone(), + is_dynamic: decl.provider_type == "dynamic", + }); + } + } + + Ok(regs) } /// Call a provider's refresh function @@ -107,7 +127,17 @@ impl LoadedPlugin { .as_ref() .ok_or_else(|| "Plugin not initialized".to_string())?; - api::call_refresh(lua, provider_name).map_err(|e| format!("Refresh failed: {}", e)) + let items = api::call_refresh(lua, provider_name) + .map_err(|e| format!("Refresh failed: {}", e))?; + + // If the API path returned nothing, try calling the global refresh() + // function directly (new-style plugins with manifest [[providers]]) + if items.is_empty() { + return api::call_global_refresh(lua) + .map_err(|e| format!("Refresh failed: {}", e)); + } + + Ok(items) } /// Call a provider's query function diff --git a/crates/owlry-lua/src/manifest.rs b/crates/owlry-lua/src/manifest.rs index 9b56b0e..adf23ff 100644 --- a/crates/owlry-lua/src/manifest.rs +++ b/crates/owlry-lua/src/manifest.rs @@ -8,6 +8,10 @@ use std::path::Path; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginManifest { pub plugin: PluginInfo, + /// Provider declarations from [[providers]] sections (new-style) + #[serde(default)] + pub providers: Vec, + /// Legacy provides block (old-style) #[serde(default)] pub provides: PluginProvides, #[serde(default)] @@ -16,6 +20,26 @@ pub struct PluginManifest { pub settings: HashMap, } +/// A provider declared in a [[providers]] section +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProviderDecl { + pub id: String, + pub name: String, + #[serde(default)] + pub prefix: Option, + #[serde(default)] + pub icon: Option, + /// "static" (default) or "dynamic" + #[serde(default = "default_provider_type", rename = "type")] + pub provider_type: String, + #[serde(default)] + pub type_id: Option, +} + +fn default_provider_type() -> String { + "static".to_string() +} + /// Core plugin information #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PluginInfo { diff --git a/crates/owlry/src/plugin_commands.rs b/crates/owlry/src/plugin_commands.rs index cf5a216..bc19a8f 100644 --- a/crates/owlry/src/plugin_commands.rs +++ b/crates/owlry/src/plugin_commands.rs @@ -754,10 +754,16 @@ fn cmd_create( let desc = description.unwrap_or("A custom owlry plugin"); let (entry_file, entry_ext) = match runtime { - PluginRuntime::Lua => ("init.lua", "lua"), - PluginRuntime::Rune => ("init.rn", "rn"), + PluginRuntime::Lua => ("main.lua", "lua"), + PluginRuntime::Rune => ("main.rn", "rn"), }; + // Derive a short type_id from the plugin name (strip common prefixes) + let type_id = name + .strip_prefix("owlry-") + .unwrap_or(name) + .replace('-', "_"); + // Create plugin.toml let manifest = format!( r#"[plugin] @@ -765,25 +771,21 @@ id = "{name}" name = "{display}" version = "0.1.0" description = "{desc}" -author = "" -owlry_version = ">=0.3.0" -entry = "{entry_file}" +entry_point = "{entry_file}" -[provides] -providers = ["{name}"] -actions = false -themes = [] -hooks = false - -[permissions] -network = false -filesystem = [] -run_commands = [] +[[providers]] +id = "{name}" +name = "{display}" +type = "static" +type_id = "{type_id}" +icon = "application-x-addon" +# prefix = ":{type_id}" "#, name = name, display = display, desc = desc, entry_file = entry_file, + type_id = type_id, ); fs::write(plugin_dir.join("plugin.toml"), manifest) @@ -792,91 +794,51 @@ run_commands = [] // Create entry point template based on runtime match runtime { PluginRuntime::Lua => { - let init_lua = format!( + let main_lua = format!( r#"-- {display} Plugin for Owlry -- {desc} --- Register the provider -owlry.provider.register({{ - name = "{name}", - display_name = "{display}", - type_id = "{name}", - default_icon = "application-x-executable", - - refresh = function() - -- Return a list of items - return {{ - {{ - id = "{name}:example", - name = "Example Item", - description = "This is an example item from {display}", - icon = "dialog-information", - command = "echo 'Hello from {name}!'", - terminal = false, - tags = {{}} - }} - }} - end -}}) - -owlry.log.info("{display} plugin loaded") +function refresh() + return {{ + {{ + id = "{name}:example", + name = "Example Item", + description = "This is an example item from {display}", + icon = "dialog-information", + command = "echo 'Hello from {name}!'", + tags = {{}}, + }}, + }} +end "#, name = name, display = display, desc = desc, ); - fs::write(plugin_dir.join(entry_file), init_lua) + fs::write(plugin_dir.join(entry_file), main_lua) .map_err(|e| format!("Failed to write {}: {}", entry_file, e))?; } PluginRuntime::Rune => { - // Note: Rune uses #{{ for object literals, so we build manually - let init_rn = format!( - r#"//! {display} Plugin for Owlry -//! {desc} + let main_rn = format!( + r#"use owlry::Item; -/// Plugin item structure -struct Item {{{{ - id: String, - name: String, - description: String, - icon: String, - command: String, - terminal: bool, - tags: Vec, -}}}} +pub fn refresh() {{ + let items = []; -/// Provider registration -pub fn register(owlry) {{{{ - owlry.provider.register(#{{{{ - name: "{name}", - display_name: "{display}", - type_id: "{name}", - default_icon: "application-x-executable", + items.push( + Item::new("{name}:example", "Example Item", "echo 'Hello from {name}!'") + .description("This is an example item from {display}") + .icon("dialog-information") + .keywords(["example"]), + ); - refresh: || {{{{ - // Return a list of items - [ - Item {{{{ - id: "{name}:example", - name: "Example Item", - description: "This is an example item from {display}", - icon: "dialog-information", - command: "echo 'Hello from {name}!'", - terminal: false, - tags: [], - }}}}, - ] - }}}}, - }}}}); - - owlry.log.info("{display} plugin loaded"); -}}}} + items +}} "#, name = name, display = display, - desc = desc, ); - fs::write(plugin_dir.join(entry_file), init_rn) + fs::write(plugin_dir.join(entry_file), main_rn) .map_err(|e| format!("Failed to write {}: {}", entry_file, e))?; } } @@ -955,13 +917,14 @@ fn cmd_validate(path: Option<&str>) -> CommandResult { )); } - // Check for empty provides - if manifest.provides.providers.is_empty() - && !manifest.provides.actions - && manifest.provides.themes.is_empty() - && !manifest.provides.hooks - { - warnings.push("Plugin does not provide any features".to_string()); + // Check for empty provides (accept either [[providers]] or [provides]) + let has_providers = !manifest.providers.is_empty() + || !manifest.provides.providers.is_empty() + || manifest.provides.actions + || !manifest.provides.themes.is_empty() + || manifest.provides.hooks; + if !has_providers { + warnings.push("Plugin does not declare any providers".to_string()); } println!(" Plugin ID: {}", manifest.plugin.id);