feat: convert to workspace with native plugin architecture

BREAKING: Restructure from monolithic binary to modular plugin ecosystem

Architecture changes:
- Convert to Cargo workspace with crates/ directory
- Create owlry-plugin-api crate with ABI-stable interface (abi_stable)
- Move core binary to crates/owlry/
- Extract providers to native plugin crates (13 plugins)
- Add owlry-lua crate for Lua plugin runtime

Plugin system:
- Plugins loaded from /usr/lib/owlry/plugins/*.so
- Widget providers refresh automatically (universal, not hardcoded)
- Per-plugin config via [plugins.<name>] sections in config.toml
- Backwards compatible with [providers] config format

New features:
- just install-local: build and install core + all plugins
- Plugin config: weather and pomodoro read from [plugins.*]
- HostAPI for plugins: notifications, logging

Documentation:
- Update README with new package structure
- Add docs/PLUGINS.md with all plugin documentation
- Add docs/PLUGIN_DEVELOPMENT.md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 03:01:37 +01:00
parent a582f0181c
commit 384dd016a0
124 changed files with 18609 additions and 3692 deletions

330
docs/PLUGINS.md Normal file
View File

@@ -0,0 +1,330 @@
# Available Plugins
Owlry's functionality is provided through a modular plugin system. This document describes all available plugins.
## Plugin Categories
### Static Providers
Static providers load their items once at startup (and on manual refresh). They're best for data that doesn't change frequently.
### Dynamic Providers
Dynamic providers evaluate queries in real-time. Each keystroke triggers a new query, making them ideal for calculations, searches, and other interactive features.
### Widget Providers
Widget providers display persistent information at the top of results (weather, media controls, timers).
---
## Core Plugins
### owlry-plugin-calculator
**Type:** Dynamic
**Prefix:** `:calc`, `=`, `calc `
**Package:** `owlry-plugin-calculator`
Evaluate mathematical expressions in real-time.
**Examples:**
```
= 5 + 3 → 8
= sqrt(16) → 4
= sin(pi/2) → 1
= 2^10 → 1024
= (1 + 0.05)^12 → 1.7958...
```
**Supported operations:**
- Basic: `+`, `-`, `*`, `/`, `^` (power), `%` (modulo)
- Functions: `sin`, `cos`, `tan`, `asin`, `acos`, `atan`
- Functions: `sqrt`, `abs`, `floor`, `ceil`, `round`
- Functions: `ln`, `log`, `log10`, `exp`
- Constants: `pi`, `e`
---
### owlry-plugin-system
**Type:** Static
**Prefix:** `:sys`
**Package:** `owlry-plugin-system`
System power and session management commands.
**Actions:**
| Name | Description | Command |
|------|-------------|---------|
| Shutdown | Power off | `systemctl poweroff` |
| Reboot | Restart | `systemctl reboot` |
| Reboot into BIOS | UEFI setup | `systemctl reboot --firmware-setup` |
| Suspend | Sleep (RAM) | `systemctl suspend` |
| Hibernate | Sleep (disk) | `systemctl hibernate` |
| Lock Screen | Lock session | `loginctl lock-session` |
| Log Out | End session | `loginctl terminate-session self` |
---
### owlry-plugin-ssh
**Type:** Static
**Prefix:** `:ssh`
**Package:** `owlry-plugin-ssh`
SSH hosts parsed from `~/.ssh/config`.
**Features:**
- Parses `Host` entries from SSH config
- Ignores wildcards (`Host *`)
- Opens connections in your configured terminal
---
### owlry-plugin-clipboard
**Type:** Static
**Prefix:** `:clip`
**Package:** `owlry-plugin-clipboard`
**Dependencies:** `cliphist`, `wl-clipboard`
Clipboard history integration with cliphist.
**Features:**
- Shows last 50 clipboard entries
- Previews text content (truncated to 80 chars)
- Select to copy back to clipboard
---
### owlry-plugin-emoji
**Type:** Static
**Prefix:** `:emoji`
**Package:** `owlry-plugin-emoji`
**Dependencies:** `wl-clipboard`
400+ searchable emoji with keywords.
**Examples:**
```
:emoji heart → ❤️ 💙 💚 💜 ...
:emoji smile → 😀 😃 😄 😁 ...
:emoji fire → 🔥
```
---
### owlry-plugin-scripts
**Type:** Static
**Prefix:** `:script`
**Package:** `owlry-plugin-scripts`
User scripts from `~/.local/share/owlry/scripts/`.
**Setup:**
```bash
mkdir -p ~/.local/share/owlry/scripts
cat > ~/.local/share/owlry/scripts/backup.sh << 'EOF'
#!/bin/bash
rsync -av ~/Documents /backup/
notify-send "Backup complete"
EOF
chmod +x ~/.local/share/owlry/scripts/backup.sh
```
---
### owlry-plugin-bookmarks
**Type:** Static
**Prefix:** `:bm`
**Package:** `owlry-plugin-bookmarks`
Browser bookmarks from Chromium-based browsers.
**Supported browsers:**
- Google Chrome
- Brave
- Microsoft Edge
- Vivaldi
- Chromium
---
### owlry-plugin-websearch
**Type:** Dynamic
**Prefix:** `:web`, `?`, `web `
**Package:** `owlry-plugin-websearch`
Web search with configurable search engine.
**Examples:**
```
? rust programming → Search for "rust programming"
web linux tips → Search for "linux tips"
```
**Configuration:**
```toml
[providers]
search_engine = "duckduckgo" # or: google, bing, startpage
# custom_search_url = "https://search.example.com/?q={}"
```
---
### owlry-plugin-filesearch
**Type:** Dynamic
**Prefix:** `:file`, `/`, `find `
**Package:** `owlry-plugin-filesearch`
**Dependencies:** `fd` (recommended) or `mlocate`
Real-time file search.
**Examples:**
```
/ .bashrc → Find files matching ".bashrc"
find config → Find files matching "config"
```
**Configuration:**
```toml
[providers]
file_search_max_results = 50
# file_search_paths = ["/home", "/etc"] # Custom search paths
```
---
### owlry-plugin-systemd
**Type:** Static (with submenu)
**Prefix:** `:uuctl`
**Package:** `owlry-plugin-systemd`
**Dependencies:** `systemd`
User systemd services with action submenus.
**Features:**
- Lists user services (`systemctl --user`)
- Shows service status (running/stopped/failed)
- Submenu actions: start, stop, restart, enable, disable, status
**Usage:**
1. Search `:uuctl docker`
2. Select a service
3. Choose action from submenu
---
## Widget Plugins
### owlry-plugin-weather
**Type:** Widget (Static)
**Package:** `owlry-plugin-weather`
Current weather displayed at the top of results.
**Supported APIs:**
- wttr.in (default, no API key required)
- OpenWeatherMap (requires API key)
- Open-Meteo (no API key required)
**Configuration:**
```toml
[plugins.weather]
provider = "wttr.in" # or: openweathermap, open-meteo
location = "London" # city name or "lat,lon" (empty for auto-detect)
# api_key = "..." # Required for OpenWeatherMap
```
**Features:**
- Temperature, condition, humidity, wind speed
- Weather icons from Weather Icons font
- 15-minute cache
- Click to open detailed forecast
---
### owlry-plugin-media
**Type:** Widget (Static)
**Package:** `owlry-plugin-media`
MPRIS media player controls.
**Features:**
- Shows currently playing track
- Artist, title, album art
- Play/pause, next, previous controls
- Works with Spotify, Firefox, VLC, etc.
---
### owlry-plugin-pomodoro
**Type:** Widget (Static)
**Package:** `owlry-plugin-pomodoro`
Pomodoro timer with work/break cycles.
**Configuration:**
```toml
[plugins.pomodoro]
work_mins = 25 # Work session duration (default: 25)
break_mins = 5 # Break duration (default: 5)
```
**Features:**
- Configurable work session duration
- Configurable break duration
- Session counter
- Desktop notifications on phase completion
- Persistent state across sessions
**Controls:**
- Start/Pause timer
- Skip to next phase
- Reset timer and sessions
---
## Bundle Packages
For convenience, plugins are available in bundle meta-packages:
| Bundle | Plugins |
|--------|---------|
| `owlry-essentials` | calculator, system, ssh, scripts, bookmarks |
| `owlry-widgets` | weather, media, pomodoro |
| `owlry-tools` | clipboard, emoji, websearch, filesearch, systemd |
| `owlry-full` | All of the above |
```bash
# Install everything
yay -S owlry-full
# Or pick a bundle
yay -S owlry-essentials owlry-widgets
```
---
## Runtime Packages
For custom user plugins written in Lua or Rune:
| Package | Description |
|---------|-------------|
| `owlry-lua` | Lua 5.4 runtime for user plugins |
| `owlry-rune` | Rune runtime for user plugins |
User plugins are placed in `~/.config/owlry/plugins/`.
See [PLUGIN_DEVELOPMENT.md](PLUGIN_DEVELOPMENT.md) for creating custom plugins.

562
docs/PLUGIN_DEVELOPMENT.md Normal file
View File

@@ -0,0 +1,562 @@
# 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 = "2024"
[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, 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<ProviderInfo> {
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"),
}].into()
}
extern "C" fn provider_init(_provider_id: RStr<'_>) -> ProviderHandle {
ProviderHandle::null()
}
extern "C" fn provider_refresh(_handle: ProviderHandle) -> RVec<PluginItem> {
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<PluginItem> {
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_point = "init.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/init.lua`:
```lua
local owlry = require("owlry")
-- Called once at startup for static providers
function refresh()
return {
owlry.item("item-1", "Hello from Lua", "echo 'Hello Lua!'")
:description("A Lua greeting")
:icon("face-smile"),
}
end
-- Called per-keystroke for dynamic providers
function query(q)
return {}
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<ProviderInfo>,
pub provider_init: extern "C" fn(provider_id: RStr<'_>) -> ProviderHandle,
pub provider_refresh: extern "C" fn(handle: ProviderHandle) -> RVec<PluginItem>,
pub provider_query: extern "C" fn(handle: ProviderHandle, query: RStr<'_>) -> RVec<PluginItem>,
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<RString>, // 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 enum ProviderKind {
Static, // Items loaded at startup via refresh()
Dynamic, // Items computed per-query via query()
}
```
### PluginItem
```rust
pub struct PluginItem {
pub id: RString, // Unique item ID
pub name: RString, // Display name
pub description: ROption<RString>, // Optional description
pub icon: ROption<RString>, // Optional icon
pub command: RString, // Command to execute
pub terminal: bool, // Run in terminal?
pub keywords: RVec<RString>, // 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<PluginItem>,
cache: HashMap<String, String>,
}
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<PluginItem> {
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::<MyState>(); }
}
}
```
### 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<PluginItem> {
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_point = "init.lua"
owlry_version = ">=0.4.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
local owlry = require("owlry")
-- Create items
local item = owlry.item(id, name, command)
:description("Description")
:icon("icon-name")
:terminal(false)
:keywords({"tag1", "tag2"})
-- 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
function refresh()
return {
owlry.item("id1", "Item 1", "command1"),
owlry.item("id2", "Item 2", "command2"),
}
end
-- Dynamic provider: called on each keystroke
function query(q)
if q == "" then
return {}
end
return {
owlry.item("result", "Result for: " .. q, "echo " .. q),
}
end
```
---
## Rune Plugin API
Rune plugins use a Rust-like syntax with memory safety.
### Plugin Manifest
```toml
[plugin]
id = "my-rune-plugin"
name = "My Rune Plugin"
version = "1.0.0"
entry_point = "main.rn"
[[providers]]
id = "runeprovider"
name = "Rune Provider"
type = "static"
```
### Rune API
```rune
use owlry::{Item, log, notify};
pub fn refresh() {
let items = [];
items.push(Item::new("id", "Name", "command")
.description("Description")
.icon("icon-name"));
items
}
pub fn query(q) {
if q.is_empty() {
return [];
}
log::info(`Query: {q}`);
[Item::new("result", `Result: {q}`, `echo {q}`)]
}
```
---
## 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<PluginItem> {
match load_data() {
Ok(items) => items.into(),
Err(e) => {
log_error(&format!("Failed to load: {}", e));
RVec::new()
}
}
}
```
```lua
-- Lua: Wrap in pcall for safety
function refresh()
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
```
---
## Publishing to AUR
### PKGBUILD Template
```bash
# Maintainer: Your Name <email@example.com>
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.