Replaces five while-loop child removal patterns with the batched remove_all() method available since GTK 4.12. Avoids per-removal layout invalidation.
7.0 KiB
Script Runtime Integration for owlry-core Daemon
Date: 2026-03-26 Scope: Wire up Lua/Rune script runtime loading in the daemon, fix ABI mismatch, add filesystem-watching hot-reload, update plugin documentation Repos: owlry (core), owlry-plugins (docs only)
Problem
The daemon (owlry-core) only loads native plugins from /usr/lib/owlry/plugins/. User script plugins in ~/.config/owlry/plugins/ are never discovered because ProviderManager::new_with_config() never calls the LoadedRuntime infrastructure that already exists in runtime_loader.rs. Both Lua and Rune runtimes are installed at /usr/lib/owlry/runtimes/ and functional, but never invoked.
Additionally, the Lua runtime's RuntimeInfo struct has 5 fields while the core expects 2, causing a SIGSEGV on cleanup.
1. Fix Lua RuntimeInfo ABI mismatch
File: owlry/crates/owlry-lua/src/lib.rs
Shrink Lua's RuntimeInfo from 5 fields to 2, matching core and Rune:
// Before (5 fields — ABI mismatch with core):
pub struct RuntimeInfo {
pub id: RString,
pub name: RString,
pub version: RString,
pub description: RString,
pub api_version: u32,
}
// After (2 fields — matches core/Rune):
pub struct RuntimeInfo {
pub name: RString,
pub version: RString,
}
Update runtime_info() to return only 2 fields. Remove the LUA_RUNTIME_API_VERSION constant and LuaRuntimeVTable (use the core's ScriptRuntimeVTable layout — both already match). The extra metadata (id, description) was never consumed by the core.
Vtable init signature change
Change the init function in the vtable to accept the owlry version as a second parameter:
// Before:
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
// After:
pub init: extern "C" fn(plugins_dir: RStr<'_>, owlry_version: RStr<'_>) -> RuntimeHandle,
This applies to:
owlry-core/src/plugins/runtime_loader.rs—ScriptRuntimeVTable.initowlry-lua/src/lib.rs—LuaRuntimeVTable.initandruntime_init()implementationowlry-rune/src/lib.rs—RuneRuntimeVTable.initandruntime_init()implementation
The core passes its version (env!("CARGO_PKG_VERSION") from owlry-core) when calling (vtable.init)(plugins_dir, version). Runtimes forward it to discover_and_load() instead of hardcoding a version string. This keeps compatibility checks future-proof — no code changes needed on version bumps.
2. Change default entry points to main
Files:
owlry/crates/owlry-lua/src/manifest.rs— changedefault_entry()from"init.lua"to"main.lua"owlry/crates/owlry-rune/src/manifest.rs— changedefault_entry()from"init.rn"to"main.rn"
Add #[serde(alias = "entry_point")] to the entry field in both manifests so existing plugin.toml files using entry_point continue to work.
3. Wire runtime loading into ProviderManager
File: owlry/crates/owlry-core/src/providers/mod.rs
In ProviderManager::new_with_config(), after native plugin loading:
- Get user plugins directory from
paths::plugins_dir() - Get owlry version:
env!("CARGO_PKG_VERSION") - Try
LoadedRuntime::load_lua(&plugins_dir, version)— log atinfo!if unavailable, not error - Try
LoadedRuntime::load_rune(&plugins_dir, version)— same - Call
create_providers()on each loaded runtime - Feed runtime providers into existing categorization (static/dynamic/widget)
LoadedRuntime::load_lua, load_rune, and load_from_path all gain an owlry_version: &str parameter, which is passed to (vtable.init)(plugins_dir, owlry_version).
Store LoadedRuntime instances on ProviderManager in a new field runtimes: Vec<LoadedRuntime>. These must stay alive for the daemon's lifetime (they own the Library handle via Arc).
Remove #![allow(dead_code)] from runtime_loader.rs since it's now used.
4. Filesystem watcher for automatic hot-reload
New file: owlry/crates/owlry-core/src/plugins/watcher.rs
Modified: owlry/crates/owlry-core/src/providers/mod.rs, Cargo.toml
Dependencies
Add to owlry-core/Cargo.toml:
notify = "7"
notify-debouncer-mini = "0.5"
Watcher design
After initializing runtimes, spawn a background watcher thread:
- Watch
~/.config/owlry/plugins/recursively usingnotify-debouncer-miniwith 500ms debounce - On debounced event (any file create/modify/delete):
- Acquire write lock on
ProviderManager - Remove all runtime-backed providers from the provider vecs
- Drop old
LoadedRuntimeinstances - Re-load runtimes from
/usr/lib/owlry/runtimes/with fresh plugin discovery - Add new runtime providers to provider vecs
- Refresh the new providers
- Release write lock
- Acquire write lock on
Provider tracking
ProviderManager needs to distinguish runtime providers from native/core providers for selective removal during reload. Options:
- Tag-based: Runtime providers already use
ProviderType::Plugin(type_id). Keep aHashSet<String>of type_ids that came from runtimes. On reload, remove providers whose type_id is in the set. - Separate storage: Store runtime providers in their own vec, separate from native providers. Query merges results from both.
Chosen: Tag-based. Simpler — runtime type_ids are tracked in a runtime_type_ids: HashSet<String> on ProviderManager. Reload clears the set, removes matching providers, then re-adds.
Thread communication
The watcher thread needs access to Arc<RwLock<ProviderManager>>. The Server already holds this Arc. Pass a clone to the watcher thread at startup. The watcher acquires write() only during reload (~10ms), so read contention is minimal.
Watcher lifecycle
- Started in
Server::run()(orServer::bind()) before the accept loop - Runs until the daemon exits (watcher thread is detached or joined on drop)
- Errors in the watcher (e.g., inotify limit exceeded) are logged and the watcher stops — daemon continues without hot-reload
5. Plugin development documentation
File: owlry-plugins/docs/PLUGIN_DEVELOPMENT.md
Cover:
- Plugin directory structure —
~/.config/owlry/plugins/<name>/plugin.toml+main.lua/main.rn - Manifest reference — all
plugin.tomlfields (id,name,version,description,entry/entry_point,owlry_version,[[providers]]section,[permissions]section) - Lua plugin guide —
owlry.provider.register()API withrefreshandquerycallbacks, item table format (id,name,command,description,icon,terminal,tags) - Rune plugin guide —
pub fn refresh()andpub fn query(q)signatures,Item::new()builder,use owlry::Item - Hot-reload — changes are picked up automatically, no daemon restart needed
- Examples — complete working examples for both Lua and Rune
Out of scope
- Config-gated runtime loading (runtimes self-skip if
.sonot installed) - Per-plugin selective reload (full runtime reload is fast enough)
- Plugin registry/installation (already exists in the CLI)
- Sandbox enforcement (separate concern, deferred from hardening spec)