Compare commits

..

9 Commits

Author SHA1 Message Date
4d7e913657 chore(aur): update all packages to latest versions 2026-04-06 02:42:09 +02:00
f8d011447e chore(owlry): bump version to 1.0.8 2026-04-06 02:39:19 +02:00
9163b1ea6c chore(owlry-rune): bump version to 1.1.4 2026-04-06 02:38:47 +02:00
6586f5d6c2 fix(plugins): close remaining gaps in new plugin format support
- Fix cmd_runtimes() help text: init.lua/init.rn → main.lua/main.rn
- Add .lua extension validation to owlry-lua manifest (mirrors Rune)
- Replace eprintln! with log::warn!/log::debug! in owlry-lua loader
- Add log = "0.4" dependency to owlry-lua
- Add tests: [[providers]] deserialization in owlry-core manifest,
  manifest provider fallback in owlry-lua and owlry-rune loaders,
  non-runtime plugin filtering in both runtimes
2026-04-06 02:38:42 +02:00
a6e94deb3c fix(runtime): prevent dlclose() to avoid SIGSEGV on runtime teardown
Wrap LoadedRuntime._library in ManuallyDrop so dlclose() is never called.
dlclose() unmaps the library code; thread-local destructors inside liblua.so
then SIGSEGV when they try to run against the unmapped addresses.

Also filter out non-.lua plugins in the Lua runtime's discover_plugins()
so liblua.so does not attempt to load Rune plugins.
2026-04-06 02:26:12 +02:00
de74cac67d chore(owlry-lua): bump version to 1.1.3 2026-04-06 02:22:08 +02:00
2f396306fd chore(owlry-core): bump version to 1.3.4 2026-04-06 02:22:07 +02:00
133d5264ea 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
2026-04-06 02:22:03 +02:00
a16c3a0523 fix(just): skip meta packages without PKGBUILD in aur-publish-all 2026-04-06 02:11:32 +02:00
30 changed files with 487 additions and 138 deletions

9
Cargo.lock generated
View File

@@ -2348,7 +2348,7 @@ dependencies = [
[[package]]
name = "owlry"
version = "1.0.7"
version = "1.0.8"
dependencies = [
"chrono",
"clap",
@@ -2369,7 +2369,7 @@ dependencies = [
[[package]]
name = "owlry-core"
version = "1.3.3"
version = "1.3.4"
dependencies = [
"chrono",
"ctrlc",
@@ -2397,11 +2397,12 @@ dependencies = [
[[package]]
name = "owlry-lua"
version = "1.1.2"
version = "1.1.3"
dependencies = [
"abi_stable",
"chrono",
"dirs",
"log",
"meval",
"mlua",
"owlry-plugin-api",
@@ -2423,7 +2424,7 @@ dependencies = [
[[package]]
name = "owlry-rune"
version = "1.1.3"
version = "1.1.4"
dependencies = [
"chrono",
"dirs",

View File

@@ -1,6 +1,6 @@
pkgbase = owlry-core
pkgdesc = Core daemon for the Owlry application launcher — manages plugins, providers, and search
pkgver = 1.3.2
pkgver = 1.3.4
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
@@ -8,7 +8,7 @@ pkgbase = owlry-core
makedepends = cargo
depends = gcc-libs
depends = openssl
source = owlry-core-1.3.2.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.3.2.tar.gz
b2sums = 36a1e31cadcfdbe70c0a10c13eddbcea7ae21b7dcfb0aa10a75f44a82a377d6598c4237228457c13260ca4b4b88f12d416541ad7698cf28076124b1a4d3dbbc6
source = owlry-core-1.3.4.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v1.3.4.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry-core

10
aur/owlry-core/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-core
pkgver=1.3.2
pkgver=1.3.4
pkgrel=1
pkgdesc='Core daemon for the Owlry application launcher — manages plugins, providers, and search'
arch=('x86_64')
@@ -9,7 +9,7 @@ license=('GPL-3.0-or-later')
depends=('gcc-libs' 'openssl')
makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-core-v$pkgver.tar.gz")
b2sums=('36a1e31cadcfdbe70c0a10c13eddbcea7ae21b7dcfb0aa10a75f44a82a377d6598c4237228457c13260ca4b4b88f12d416541ad7698cf28076124b1a4d3dbbc6')
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
prepare() {
cd "owlry"

View File

@@ -1,6 +1,6 @@
pkgbase = owlry-lua
pkgdesc = Lua scripting runtime for Owlry — enables user-created Lua plugins
pkgver = 1.1.2
pkgver = 1.1.3
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
@@ -8,7 +8,7 @@ pkgbase = owlry-lua
makedepends = cargo
depends = owlry-core
depends = openssl
source = owlry-lua-1.1.2.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v1.1.2.tar.gz
b2sums = 42e6221e6e07c629ece1493e7f5feb1b2cb2e77632d1d7779dfbe544bd89a17d77d1839d63e50d71d4f0e0322ca8a1cc39b872101039019bdf08d9bcaeda7603
source = owlry-lua-1.1.3.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v1.1.3.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry-lua

10
aur/owlry-lua/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-lua
pkgver=1.1.2
pkgver=1.1.3
pkgrel=1
pkgdesc="Lua scripting runtime for Owlry — enables user-created Lua plugins"
arch=('x86_64')
@@ -9,7 +9,7 @@ license=('GPL-3.0-or-later')
depends=('owlry-core' 'openssl')
makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-lua-v$pkgver.tar.gz")
b2sums=('42e6221e6e07c629ece1493e7f5feb1b2cb2e77632d1d7779dfbe544bd89a17d77d1839d63e50d71d4f0e0322ca8a1cc39b872101039019bdf08d9bcaeda7603')
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
_cratename=owlry-lua

Submodule aur/owlry-meta-essentials updated: 4a09cfb73c...ed91b61709

Submodule aur/owlry-meta-full updated: 8f85087731...2115aa08f8

Submodule aur/owlry-meta-tools updated: 28c78b7953...bc821ff47f

Submodule aur/owlry-meta-widgets updated: aa4c2cd217...8ba6dd318c

View File

@@ -1,6 +1,6 @@
pkgbase = owlry-rune
pkgdesc = Rune scripting runtime for Owlry — enables user-created Rune plugins
pkgver = 1.1.3
pkgver = 1.1.4
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
@@ -8,7 +8,7 @@ pkgbase = owlry-rune
makedepends = cargo
depends = owlry-core
depends = openssl
source = owlry-rune-1.1.3.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v1.1.3.tar.gz
b2sums = 3dd1db5783b0e0c814f8e6064fd5d8cb736da5bd4759d648f48ea69a476451f64d27ecd0894378f3e41e7cc85557b7d9dcb0adf80114c32e561d4688aae6bb53
source = owlry-rune-1.1.4.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v1.1.4.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry-rune

10
aur/owlry-rune/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-rune
pkgver=1.1.3
pkgver=1.1.4
pkgrel=1
pkgdesc="Rune scripting runtime for Owlry — enables user-created Rune plugins"
arch=('x86_64')
@@ -9,7 +9,7 @@ license=('GPL-3.0-or-later')
depends=('owlry-core' 'openssl')
makedepends=('cargo')
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-rune-v$pkgver.tar.gz")
b2sums=('3dd1db5783b0e0c814f8e6064fd5d8cb736da5bd4759d648f48ea69a476451f64d27ecd0894378f3e41e7cc85557b7d9dcb0adf80114c32e561d4688aae6bb53')
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
_cratename=owlry-rune

View File

@@ -1,6 +1,6 @@
pkgbase = owlry
pkgdesc = Lightweight Wayland application launcher with plugin support
pkgver = 1.0.6
pkgver = 1.0.8
pkgrel = 1
url = https://somegit.dev/Owlibou/owlry
arch = x86_64
@@ -28,7 +28,7 @@ pkgbase = owlry
optdepends = owlry-plugin-pomodoro: pomodoro timer widget
optdepends = owlry-lua: Lua runtime for user plugins
optdepends = owlry-rune: Rune runtime for user plugins
source = owlry-1.0.6.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.6.tar.gz
b2sums = 8967562bda33820b282350eaad17e8194699926b721eabe978fb0b70af2a75e399866c6bfa7abb449141701bad618df56079c7e81358708b1852b1070b0b7c05
source = owlry-1.0.8.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v1.0.8.tar.gz
b2sums = 648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f
pkgname = owlry

10
aur/owlry/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
*.pkg.tar.zst
*.pkg.tar.zst-namcap.log
*-namcap.log
*-build.log
*-check.log
*-package.log
*-prepare.log
*.tar.gz
src/
pkg/

View File

@@ -1,6 +1,6 @@
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry
pkgver=1.0.6
pkgver=1.0.8
pkgrel=1
pkgdesc="Lightweight Wayland application launcher with plugin support"
arch=('x86_64')
@@ -29,7 +29,7 @@ optdepends=(
'owlry-rune: Rune runtime for user plugins'
)
source=("$pkgname-$pkgver.tar.gz::https://somegit.dev/Owlibou/owlry/archive/owlry-v$pkgver.tar.gz")
b2sums=('8967562bda33820b282350eaad17e8194699926b721eabe978fb0b70af2a75e399866c6bfa7abb449141701bad618df56079c7e81358708b1852b1070b0b7c05')
b2sums=('648171ce688761babb7ada9ec96cb248fab5563cc45599f660f21e166bfb4db689cff22b82f3a1f2ae256dd54fb3d3f4d5a8acaf6a728976d42ee511e1f25e5f')
prepare() {
cd "owlry"

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-core"
version = "1.3.3"
version = "1.3.4"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

View File

@@ -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]
@@ -317,6 +341,70 @@ api_url = "https://api.example.com"
assert_eq!(manifest.permissions.run_commands, vec!["myapp"]);
}
#[test]
fn test_parse_new_format_with_providers_array() {
let toml_str = r#"
[plugin]
id = "my-plugin"
name = "My Plugin"
version = "0.1.0"
description = "Test"
entry_point = "main.rn"
[[providers]]
id = "my-plugin"
name = "My Plugin"
type = "static"
type_id = "myplugin"
icon = "system-run"
prefix = ":mp"
"#;
let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.plugin.entry, "main.rn");
assert_eq!(manifest.providers.len(), 1);
let p = &manifest.providers[0];
assert_eq!(p.id, "my-plugin");
assert_eq!(p.name, "My Plugin");
assert_eq!(p.provider_type, "static");
assert_eq!(p.type_id.as_deref(), Some("myplugin"));
assert_eq!(p.icon.as_deref(), Some("system-run"));
assert_eq!(p.prefix.as_deref(), Some(":mp"));
}
#[test]
fn test_parse_new_format_entry_point_alias() {
let toml_str = r#"
[plugin]
id = "test"
name = "Test"
version = "1.0.0"
entry_point = "main.lua"
"#;
let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.plugin.entry, "main.lua");
}
#[test]
fn test_provider_spec_defaults() {
let toml_str = r#"
[plugin]
id = "test"
name = "Test"
version = "1.0.0"
[[providers]]
id = "test"
name = "Test"
"#;
let manifest: PluginManifest = toml::from_str(toml_str).unwrap();
assert_eq!(manifest.providers.len(), 1);
let p = &manifest.providers[0];
assert_eq!(p.provider_type, "static"); // default
assert!(p.prefix.is_none());
assert!(p.icon.is_none());
assert!(p.type_id.is_none());
}
#[test]
fn test_version_compatibility() {
let toml_str = r#"

View File

@@ -10,6 +10,7 @@
//! Note: This module is infrastructure for the runtime architecture. Full integration
//! is pending Phase 5 (AUR Packaging) when runtime packages will be available.
use std::mem::ManuallyDrop;
use std::path::{Path, PathBuf};
use std::sync::Arc;
@@ -69,8 +70,11 @@ pub struct ScriptRuntimeVTable {
pub struct LoadedRuntime {
/// Runtime name (for logging)
name: &'static str,
/// Keep library alive
_library: Arc<Library>,
/// Keep library alive — wrapped in ManuallyDrop so we never call dlclose().
/// dlclose() unmaps the library code; any thread-local destructors inside the
/// library then SIGSEGV when they try to run against the unmapped addresses.
/// Runtime libraries live for the process lifetime, so leaking the handle is safe.
_library: ManuallyDrop<Arc<Library>>,
/// Runtime vtable
vtable: &'static ScriptRuntimeVTable,
/// Runtime handle (state)
@@ -138,7 +142,7 @@ impl LoadedRuntime {
Ok(Self {
name,
_library: library,
_library: ManuallyDrop::new(library),
vtable,
handle,
providers,
@@ -166,6 +170,8 @@ impl LoadedRuntime {
impl Drop for LoadedRuntime {
fn drop(&mut self) {
(self.vtable.drop)(self.handle);
// Do NOT drop _library: ManuallyDrop ensures dlclose() is never called.
// See field comment for rationale.
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-lua"
version = "1.1.2"
version = "1.1.3"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
@@ -30,6 +30,9 @@ serde_json = "1.0"
# Version compatibility
semver = "1"
# Logging
log = "0.4"
# HTTP client for plugins
reqwest = { version = "0.13", default-features = false, features = ["native-tls", "blocking", "json"] }

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
@@ -156,9 +186,18 @@ pub fn discover_plugins(
match PluginManifest::load(&manifest_path) {
Ok(manifest) => {
// Skip plugins whose entry point is not a Lua file
if !manifest.plugin.entry.ends_with(".lua") {
log::debug!(
"owlry-lua: Skipping non-Lua plugin at {} (entry: {})",
path.display(),
manifest.plugin.entry
);
continue;
}
let id = manifest.plugin.id.clone();
if plugins.contains_key(&id) {
eprintln!(
log::warn!(
"owlry-lua: Duplicate plugin ID '{}', skipping {}",
id,
path.display()
@@ -168,7 +207,7 @@ pub fn discover_plugins(
plugins.insert(id, (manifest, path));
}
Err(e) => {
eprintln!(
log::warn!(
"owlry-lua: Failed to load plugin at {}: {}",
path.display(),
e
@@ -229,4 +268,79 @@ version = "1.0.0"
let plugins = discover_plugins(Path::new("/nonexistent/path")).unwrap();
assert!(plugins.is_empty());
}
#[test]
fn test_discover_skips_non_lua_plugins() {
let temp = TempDir::new().unwrap();
let plugins_dir = temp.path();
// Rune plugin — should be skipped by the Lua runtime
let rune_dir = plugins_dir.join("rune-plugin");
fs::create_dir_all(&rune_dir).unwrap();
fs::write(
rune_dir.join("plugin.toml"),
r#"
[plugin]
id = "rune-plugin"
name = "Rune Plugin"
version = "1.0.0"
entry_point = "main.rn"
[[providers]]
id = "rune-plugin"
name = "Rune Plugin"
"#,
)
.unwrap();
fs::write(rune_dir.join("main.rn"), "pub fn refresh() { [] }").unwrap();
// Lua plugin — should be discovered
create_test_plugin(plugins_dir, "lua-plugin");
let plugins = discover_plugins(plugins_dir).unwrap();
assert_eq!(plugins.len(), 1);
assert!(plugins.contains_key("lua-plugin"));
assert!(!plugins.contains_key("rune-plugin"));
}
#[test]
fn test_manifest_provider_fallback() {
let temp = TempDir::new().unwrap();
let plugin_dir = temp.path().join("test-plugin");
fs::create_dir_all(&plugin_dir).unwrap();
fs::write(
plugin_dir.join("plugin.toml"),
r#"
[plugin]
id = "test-plugin"
name = "Test Plugin"
version = "1.0.0"
entry_point = "main.lua"
[[providers]]
id = "test-plugin"
name = "Test Plugin"
type = "static"
type_id = "testplugin"
icon = "system-run"
prefix = ":tp"
"#,
)
.unwrap();
// Script that does NOT call owlry.provider.register()
fs::write(plugin_dir.join("main.lua"), "function refresh() return {} end").unwrap();
let manifest =
crate::manifest::PluginManifest::load(&plugin_dir.join("plugin.toml")).unwrap();
let mut plugin = LoadedPlugin::new(manifest, plugin_dir);
plugin.initialize().unwrap();
let regs = plugin.get_provider_registrations().unwrap();
assert_eq!(regs.len(), 1, "should fall back to [[providers]] declaration");
assert_eq!(regs[0].name, "test-plugin");
assert_eq!(regs[0].type_id, "testplugin");
assert_eq!(regs[0].prefix.as_deref(), Some(":tp"));
assert!(!regs[0].is_dynamic);
}
}

View File

@@ -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 {
@@ -127,6 +151,11 @@ impl PluginManifest {
));
}
// Lua plugins must have a .lua entry point
if !self.plugin.entry.ends_with(".lua") {
return Err("Entry point must be a .lua file for Lua plugins".to_string());
}
Ok(())
}

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-rune"
version = "1.1.3"
version = "1.1.4"
edition = "2024"
rust-version = "1.90"
description = "Rune scripting runtime for owlry plugins"

View File

@@ -203,6 +203,7 @@ pub fn discover_rune_plugins(plugins_dir: &Path) -> Result<HashMap<String, Loade
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
@@ -211,4 +212,81 @@ mod tests {
let plugins = discover_rune_plugins(temp.path()).unwrap();
assert!(plugins.is_empty());
}
#[test]
fn test_discover_skips_non_rune_plugins() {
let temp = TempDir::new().unwrap();
let plugins_dir = temp.path();
// Lua plugin — should be skipped by the Rune runtime
let lua_dir = plugins_dir.join("lua-plugin");
fs::create_dir_all(&lua_dir).unwrap();
fs::write(
lua_dir.join("plugin.toml"),
r#"
[plugin]
id = "lua-plugin"
name = "Lua Plugin"
version = "1.0.0"
entry_point = "main.lua"
[[providers]]
id = "lua-plugin"
name = "Lua Plugin"
"#,
)
.unwrap();
fs::write(lua_dir.join("main.lua"), "function refresh() return {} end").unwrap();
let plugins = discover_rune_plugins(plugins_dir).unwrap();
assert!(plugins.is_empty(), "Lua plugin should be skipped by Rune runtime");
}
#[test]
fn test_manifest_provider_fallback() {
let temp = TempDir::new().unwrap();
let plugin_dir = temp.path().join("test-plugin");
fs::create_dir_all(&plugin_dir).unwrap();
fs::write(
plugin_dir.join("plugin.toml"),
r#"
[plugin]
id = "test-plugin"
name = "Test Plugin"
version = "1.0.0"
entry_point = "main.rn"
[[providers]]
id = "test-plugin"
name = "Test Plugin"
type = "static"
type_id = "testplugin"
icon = "system-run"
prefix = ":tp"
"#,
)
.unwrap();
// Script that exports refresh() but doesn't call register_provider()
fs::write(
plugin_dir.join("main.rn"),
r#"use owlry::Item;
pub fn refresh() {
[]
}
"#,
)
.unwrap();
let manifest =
crate::manifest::PluginManifest::load(&plugin_dir.join("plugin.toml")).unwrap();
let plugin = LoadedPlugin::new(manifest, plugin_dir).unwrap();
let regs = plugin.provider_registrations();
assert_eq!(regs.len(), 1, "should fall back to [[providers]] declaration");
assert_eq!(regs[0].name, "test-plugin");
assert_eq!(regs[0].type_id, "testplugin");
assert_eq!(regs[0].prefix.as_deref(), Some(":tp"));
assert!(regs[0].is_static);
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry"
version = "1.0.7"
version = "1.0.8"
edition = "2024"
rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland"

View File

@@ -366,6 +366,14 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
"runtime": runtime.to_string(),
"runtime_available": runtime_available,
"path": plugin_path.display().to_string(),
"providers": manifest.providers.iter().map(|p| serde_json::json!({
"id": p.id,
"name": p.name,
"type": p.provider_type,
"type_id": p.type_id,
"prefix": p.prefix,
"icon": p.icon,
})).collect::<Vec<_>>(),
"provides": {
"providers": manifest.provides.providers,
"actions": manifest.provides.actions,
@@ -406,9 +414,13 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
);
println!("Path: {}", plugin_path.display());
println!();
println!("Provides:");
println!("Providers:");
for p in &manifest.providers {
let prefix = p.prefix.as_deref().map(|s| format!(" ({})", s)).unwrap_or_default();
println!(" {} [{}]{}", p.name, p.provider_type, prefix);
}
if !manifest.provides.providers.is_empty() {
println!(" Providers: {}", manifest.provides.providers.join(", "));
println!(" {}", manifest.provides.providers.join(", "));
}
if manifest.provides.actions {
println!(" Actions: yes");
@@ -754,10 +766,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 +783,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 +806,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 +929,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);
@@ -1013,11 +988,11 @@ fn cmd_runtimes() -> CommandResult {
if lua_available {
println!(" ✓ Lua - Installed");
println!(" Package: owlry-lua");
println!(" Entry point: init.lua");
println!(" Entry point: main.lua");
} else {
println!(" ✗ Lua - Not installed");
println!(" Install: yay -S owlry-lua");
println!(" Entry point: init.lua");
println!(" Entry point: main.lua");
}
println!();
@@ -1026,11 +1001,11 @@ fn cmd_runtimes() -> CommandResult {
if rune_available {
println!(" ✓ Rune - Installed");
println!(" Package: owlry-rune");
println!(" Entry point: init.rn");
println!(" Entry point: main.rn");
} else {
println!(" ✗ Rune - Not installed");
println!(" Install: yay -S owlry-rune");
println!(" Entry point: init.rn");
println!(" Entry point: main.rn");
}
println!();

View File

@@ -252,6 +252,7 @@ aur-publish-all:
for dir in aur/*/; do
pkg=$(basename "$dir")
[ -d "$dir/.git" ] || continue
[ -f "$dir/PKGBUILD" ] || continue
echo "=== $pkg ==="
just aur-publish-pkg "$pkg"
echo ""