# Plugin Development Guide This guide covers creating plugins for Owlry. There are three ways to extend Owlry: 1. **Native plugins** (Rust) — Best performance, ABI-stable interface 2. **Lua plugins** — Easy scripting, requires `owlry-lua` runtime 3. **Rune plugins** — Safe scripting with Rust-like syntax, requires `owlry-rune` runtime --- ## Quick Start ### Native Plugin (Rust) ```bash # Create a new plugin crate cargo new --lib owlry-plugin-myplugin cd owlry-plugin-myplugin ``` Edit `Cargo.toml`: ```toml [package] name = "owlry-plugin-myplugin" version = "0.1.0" edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] owlry-plugin-api = { git = "https://somegit.dev/Owlibou/owlry" } abi_stable = "0.11" ``` Edit `src/lib.rs`: ```rust use abi_stable::std_types::{ROption, RStr, RString, RVec}; use owlry_plugin_api::{ owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition, API_VERSION, }; extern "C" fn plugin_info() -> PluginInfo { PluginInfo { id: RString::from("myplugin"), name: RString::from("My Plugin"), version: RString::from(env!("CARGO_PKG_VERSION")), description: RString::from("A custom plugin"), api_version: API_VERSION, } } extern "C" fn plugin_providers() -> RVec { vec![ProviderInfo { id: RString::from("myplugin"), name: RString::from("My Plugin"), prefix: ROption::RSome(RString::from(":my")), icon: RString::from("application-x-executable"), provider_type: ProviderKind::Static, type_id: RString::from("myplugin"), position: ProviderPosition::Normal, priority: 0, // Use frecency-based ordering }].into() } extern "C" fn provider_init(_provider_id: RStr<'_>) -> ProviderHandle { ProviderHandle::null() } extern "C" fn provider_refresh(_handle: ProviderHandle) -> RVec { vec![ PluginItem::new("item-1", "Hello World", "echo 'Hello!'") .with_description("A greeting") .with_icon("face-smile"), ].into() } extern "C" fn provider_query(_handle: ProviderHandle, _query: RStr<'_>) -> RVec { RVec::new() } extern "C" fn provider_drop(_handle: ProviderHandle) {} owlry_plugin! { info: plugin_info, providers: plugin_providers, init: provider_init, refresh: provider_refresh, query: provider_query, drop: provider_drop, } ``` Build and install: ```bash cargo build --release sudo cp target/release/libowlry_plugin_myplugin.so /usr/lib/owlry/plugins/ ``` ### Lua Plugin ```bash # Requires owlry-lua runtime yay -S owlry-lua # Create plugin directory mkdir -p ~/.config/owlry/plugins/my-lua-plugin ``` Create `~/.config/owlry/plugins/my-lua-plugin/plugin.toml`: ```toml [plugin] id = "my-lua-plugin" name = "My Lua Plugin" version = "0.1.0" description = "A custom Lua plugin" entry = "main.lua" [[providers]] id = "myluaprovider" name = "My Lua Provider" prefix = ":mylua" icon = "application-x-executable" type = "static" type_id = "mylua" ``` Create `~/.config/owlry/plugins/my-lua-plugin/main.lua`: ```lua -- owlry table is pre-registered globally (no require needed) owlry.provider.register({ name = "myluaprovider", display_name = "My Lua Provider", type_id = "mylua", default_icon = "application-x-executable", prefix = ":mylua", refresh = function() return { { id = "item-1", name = "Hello from Lua", command = "echo 'Hello Lua!'", description = "A Lua greeting", icon = "face-smile", }, } end, }) ``` --- ## Native Plugin API ### Plugin VTable Every native plugin must export a function that returns a vtable: ```rust #[repr(C)] pub struct PluginVTable { pub info: extern "C" fn() -> PluginInfo, pub providers: extern "C" fn() -> RVec, pub provider_init: extern "C" fn(provider_id: RStr<'_>) -> ProviderHandle, pub provider_refresh: extern "C" fn(handle: ProviderHandle) -> RVec, pub provider_query: extern "C" fn(handle: ProviderHandle, query: RStr<'_>) -> RVec, pub provider_drop: extern "C" fn(handle: ProviderHandle), } ``` Use the `owlry_plugin!` macro to generate the export: ```rust owlry_plugin! { info: my_info_fn, providers: my_providers_fn, init: my_init_fn, refresh: my_refresh_fn, query: my_query_fn, drop: my_drop_fn, } ``` ### PluginInfo ```rust pub struct PluginInfo { pub id: RString, // Unique ID (e.g., "calculator") pub name: RString, // Display name pub version: RString, // Semantic version pub description: RString, // Short description pub api_version: u32, // Must match API_VERSION } ``` ### ProviderInfo ```rust pub struct ProviderInfo { pub id: RString, // Provider ID within plugin pub name: RString, // Display name pub prefix: ROption, // Activation prefix (e.g., ":calc") pub icon: RString, // Default icon name pub provider_type: ProviderKind, // Static or Dynamic pub type_id: RString, // Short ID for badges pub position: ProviderPosition, // Normal or Widget pub priority: i32, // Result ordering (higher = first) } pub enum ProviderKind { Static, // Items loaded at startup via refresh() Dynamic, // Items computed per-query via query() } pub enum ProviderPosition { Normal, // Standard results (sorted by score/frecency) Widget, // Displayed at top when query is empty } ``` ### PluginItem ```rust pub struct PluginItem { pub id: RString, // Unique item ID pub name: RString, // Display name pub description: ROption, // Optional description pub icon: ROption, // Optional icon pub command: RString, // Command to execute pub terminal: bool, // Run in terminal? pub keywords: RVec, // Search keywords pub score_boost: i32, // Frecency boost } // Builder pattern let item = PluginItem::new("id", "Name", "command") .with_description("Description") .with_icon("icon-name") .with_terminal(true) .with_keywords(vec!["tag1".to_string(), "tag2".to_string()]) .with_score_boost(100); ``` ### ProviderHandle For stateful providers, use `ProviderHandle` to store state: ```rust struct MyState { items: Vec, cache: HashMap, } extern "C" fn provider_init(_: RStr<'_>) -> ProviderHandle { let state = Box::new(MyState { items: Vec::new(), cache: HashMap::new(), }); ProviderHandle::from_box(state) } extern "C" fn provider_refresh(handle: ProviderHandle) -> RVec { if handle.ptr.is_null() { return RVec::new(); } let state = unsafe { &mut *(handle.ptr as *mut MyState) }; state.items = load_items(); state.items.clone().into() } extern "C" fn provider_drop(handle: ProviderHandle) { if !handle.ptr.is_null() { unsafe { handle.drop_as::(); } } } ``` ### Host API Plugins can use host-provided functions: ```rust use owlry_plugin_api::{notify, notify_with_icon, log_info, log_warn, log_error}; // Send notifications notify("Title", "Body text"); notify_with_icon("Title", "Body", "dialog-information"); // Logging log_info("Plugin loaded successfully"); log_warn("Cache miss, fetching data"); log_error("Failed to connect to API"); ``` ### Submenu Support Plugins can provide submenus for detailed actions: ```rust // Return an item that opens a submenu PluginItem::new( "service-docker", "Docker", "SUBMENU:systemd:docker.service", // Special command format ) // Handle submenu query (query starts with "?SUBMENU:") extern "C" fn provider_query(handle: ProviderHandle, query: RStr<'_>) -> RVec { let q = query.as_str(); if let Some(data) = q.strip_prefix("?SUBMENU:") { // Return submenu actions return vec![ PluginItem::new("start", "Start", format!("systemctl start {}", data)), PluginItem::new("stop", "Stop", format!("systemctl stop {}", data)), ].into(); } RVec::new() } ``` --- ## Lua Plugin API ### Plugin Manifest (plugin.toml) ```toml [plugin] id = "my-plugin" name = "My Plugin" version = "1.0.0" description = "Plugin description" entry = "main.lua" # Canonical field; entry_point is accepted as an alias owlry_version = ">=1.0.0" # Optional version constraint [permissions] fs = ["read"] # File system access http = true # HTTP requests process = true # Spawn processes [[providers]] id = "provider1" name = "Provider Name" prefix = ":prefix" icon = "icon-name" type = "static" # or "dynamic" type_id = "shortid" ``` ### Lua API ```lua -- owlry table is pre-registered globally (no require needed) -- Items are plain Lua tables returned from refresh/query callbacks: -- { id = "...", name = "...", command = "...", description = "...", icon = "...", terminal = false, tags = {"...", "..."} } -- Notifications owlry.notify("Title", "Body") owlry.notify_icon("Title", "Body", "icon-name") -- Logging owlry.log.info("Message") owlry.log.warn("Warning") owlry.log.error("Error") -- File operations (requires fs permission) local content = owlry.fs.read("/path/to/file") local files = owlry.fs.list("/path/to/dir") local exists = owlry.fs.exists("/path") -- HTTP requests (requires http permission) local response = owlry.http.get("https://api.example.com/data") local json = owlry.json.decode(response) -- Process execution (requires process permission) local output = owlry.process.run("ls", {"-la"}) -- Cache (persistent across sessions) owlry.cache.set("key", value, ttl_seconds) local value = owlry.cache.get("key") ``` ### Provider Functions ```lua -- Static provider: called once at startup and on reload owlry.provider.register({ name = "my-provider", display_name = "My Provider", prefix = ":my", refresh = function() return { { id = "id1", name = "Item 1", command = "command1" }, { id = "id2", name = "Item 2", command = "command2" }, } end, }) -- Dynamic provider: called on each keystroke owlry.provider.register({ name = "my-search", display_name = "My Search", prefix = "?my", query = function(q) if q == "" then return {} end return { { id = "result", name = "Result for: " .. q, command = "echo " .. q }, } end, }) ``` --- ## Rune Plugin API Rune plugins use a Rust-like syntax with memory safety. Requires `owlry-rune` runtime. ```bash # Install Rune runtime yay -S owlry-rune # Create plugin directory mkdir -p ~/.config/owlry/plugins/my-rune-plugin ``` ### Plugin Manifest Rune plugins declare providers in `[[providers]]` sections of `plugin.toml`. The runtime reads these declarations and maps them to the plugin's `refresh()` and `query()` functions. ```toml [plugin] id = "my-rune-plugin" name = "My Rune Plugin" version = "1.0.0" description = "A custom Rune plugin" entry = "main.rn" # Default: main.rn; entry_point also accepted [[providers]] id = "myrune" name = "My Rune Provider" prefix = ":myrune" # Activates with :myrune prefix in search icon = "application-x-executable" type = "static" # "static" (refresh at startup) or "dynamic" (query per keystroke) type_id = "myrune" # Short ID shown as badge in UI ``` ### Rune API ```rune use owlry::Item; /// Called once at startup and on each hot-reload for static providers pub fn refresh() { let items = []; items.push(Item::new("item-1", "Hello from Rune", "echo 'Hello!'") .description("A Rune greeting") .icon("face-smile") .keywords(["hello", "rune"])); items.push(Item::new("item-2", "Another Item", "notify-send 'Hi'") .description("Send a notification") .icon("dialog-information")); items } /// Called per keystroke for dynamic providers pub fn query(q) { if q.is_empty() { return []; } [Item::new("result", `Result: {q}`, `echo {q}`) .description("Search result")] } ``` ### Item Builder The `Item` type is provided by the `owlry` module: ```rune // Create an item with required fields let item = Item::new(id, name, command); // Optional builder methods (all return Item for chaining) item = item.description("Description text"); item = item.icon("icon-name"); // Freedesktop icon name item = item.keywords(["tag1", "tag2"]); // Search keywords ``` ### Logging ```rune owlry::log_info("Info message"); owlry::log_warn("Warning message"); owlry::log_error("Error message"); owlry::log_debug("Debug message"); ``` --- ## Best Practices ### Performance 1. **Static providers**: Do expensive work in `refresh()`, not `items()` 2. **Dynamic providers**: Keep `query()` fast (<50ms) 3. **Cache data**: Use persistent cache for API responses 4. **Lazy loading**: Don't load all items if only a few are needed ### Error Handling ```rust // Native: Return empty vec on error, log the issue extern "C" fn provider_refresh(handle: ProviderHandle) -> RVec { match load_data() { Ok(items) => items.into(), Err(e) => { log_error(&format!("Failed to load: {}", e)); RVec::new() } } } ``` ```lua -- Lua: Wrap refresh callback in pcall for safety owlry.provider.register({ name = "safe-provider", refresh = function() local ok, result = pcall(function() return load_items() end) if not ok then owlry.log.error("Failed: " .. result) return {} end return result end, }) ``` ### Icons Use freedesktop icon names for consistency: - `application-x-executable` — Generic executable - `folder` — Directories - `text-x-generic` — Text files - `face-smile` — Emoji/reactions - `system-shutdown` — Power actions - `network-server` — SSH/network - `edit-paste` — Clipboard ### Testing ```bash # Build and test native plugin cargo build --release -p owlry-plugin-myplugin cargo test -p owlry-plugin-myplugin # Install for testing sudo cp target/release/libowlry_plugin_myplugin.so /usr/lib/owlry/plugins/ # Test with verbose logging RUST_LOG=debug owlry ``` --- ## Hot Reload User plugins in `~/.config/owlry/plugins/` are automatically reloaded when files change. The daemon watches the plugins directory and reloads all script runtimes when any file is created, modified, or deleted. No daemon restart is needed. **What triggers a reload:** - Creating a new plugin directory with `plugin.toml` - Editing a plugin's script files (`main.lua`, `main.rn`, etc.) - Editing a plugin's `plugin.toml` - Deleting a plugin directory **What does NOT trigger a reload:** - Changes to native plugins (`.so` files) — these require a daemon restart - Changes to runtime libraries in `/usr/lib/owlry/runtimes/` — daemon restart needed **Reload behavior:** - All script runtimes (Lua, Rune) are fully reloaded - Existing search results may briefly show stale data during reload - Errors in plugins are logged but don't affect other plugins --- ## Publishing to AUR ### PKGBUILD Template ```bash # Maintainer: Your Name pkgname=owlry-plugin-myplugin pkgver=0.1.0 pkgrel=1 pkgdesc="My custom Owlry plugin" arch=('x86_64') url="https://github.com/you/owlry-plugin-myplugin" license=('GPL-3.0-or-later') depends=('owlry') makedepends=('rust' 'cargo') source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz") sha256sums=('...') build() { cd "$pkgname-$pkgver" cargo build --release } package() { cd "$pkgname-$pkgver" install -Dm755 "target/release/lib${pkgname//-/_}.so" \ "$pkgdir/usr/lib/owlry/plugins/lib${pkgname//-/_}.so" } ``` --- ## Example Plugins The owlry repository includes 13 native plugins as reference implementations: | Plugin | Type | Highlights | |--------|------|------------| | `owlry-plugin-calculator` | Dynamic | Math parsing, expression evaluation | | `owlry-plugin-weather` | Static/Widget | HTTP API, JSON parsing, caching | | `owlry-plugin-systemd` | Static | Submenu actions, service management | | `owlry-plugin-pomodoro` | Static/Widget | State persistence, notifications | | `owlry-plugin-clipboard` | Static | External process integration | Browse the source at `crates/owlry-plugin-*/` for implementation details.