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<ProviderDecl> 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
This commit is contained in:
@@ -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<ProviderSpec>,
|
||||
/// 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<String>,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
/// "static" (default) or "dynamic"
|
||||
#[serde(default = "default_provider_type", rename = "type")]
|
||||
pub provider_type: String,
|
||||
#[serde(default)]
|
||||
pub type_id: Option<String>,
|
||||
}
|
||||
|
||||
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]
|
||||
|
||||
@@ -50,3 +50,8 @@ pub fn call_refresh(lua: &Lua, provider_name: &str) -> LuaResult<Vec<PluginItem>
|
||||
pub fn call_query(lua: &Lua, provider_name: &str, query: &str) -> LuaResult<Vec<PluginItem>> {
|
||||
provider::call_query(lua, provider_name, query)
|
||||
}
|
||||
|
||||
/// Call the global `refresh()` function (for manifest-declared providers)
|
||||
pub fn call_global_refresh(lua: &Lua) -> LuaResult<Vec<PluginItem>> {
|
||||
provider::call_global_refresh(lua)
|
||||
}
|
||||
|
||||
@@ -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<Vec<PluginItem>> {
|
||||
let globals = lua.globals();
|
||||
match globals.get::<Function>("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<Vec<ProviderRegistration>> {
|
||||
// Suppress unused warning
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ProviderDecl>,
|
||||
/// Legacy provides block (old-style)
|
||||
#[serde(default)]
|
||||
pub provides: PluginProvides,
|
||||
#[serde(default)]
|
||||
@@ -16,6 +20,26 @@ pub struct PluginManifest {
|
||||
pub settings: HashMap<String, toml::Value>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
#[serde(default)]
|
||||
pub icon: Option<String>,
|
||||
/// "static" (default) or "dynamic"
|
||||
#[serde(default = "default_provider_type", rename = "type")]
|
||||
pub provider_type: String,
|
||||
#[serde(default)]
|
||||
pub type_id: Option<String>,
|
||||
}
|
||||
|
||||
fn default_provider_type() -> String {
|
||||
"static".to_string()
|
||||
}
|
||||
|
||||
/// Core plugin information
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PluginInfo {
|
||||
|
||||
@@ -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<String>,
|
||||
}}}}
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user