17 Commits

Author SHA1 Message Date
bf8a31af78 chore: bump all crates to 0.4.7 2026-01-01 22:29:00 +01:00
e23bdf5cee fix(providers): enable submenu support for static native plugins
Static native plugins (systemd, clipboard, etc.) were being boxed as
Box<dyn Provider>, which lost access to the query() method needed for
submenu support. The Provider trait only has refresh() and items().

Add static_native_providers field to keep static native plugins as
NativeProvider instances, preserving their query() method. Update all
search methods and query_submenu_actions() to include this new list.

Fixes systemd plugin submenu not showing actions when selecting a service.
2026-01-01 22:14:43 +01:00
25c4d40d36 docs: add comprehensive usage documentation
- Expand CLI --help with examples, dmenu mode, and search prefixes
- Add dmenu mode section to README with practical examples
- Add plugin management CLI reference to README
- Update argument descriptions with all valid modes listed
2026-01-01 21:45:52 +01:00
b36dd2a438 chore: update bump-all to include core in single commit 2025-12-30 20:32:28 +01:00
35a0f580c3 chore(owlry-rune): bump version to 0.4.6 2025-12-30 20:23:58 +01:00
7ed36c58c2 chore(owlry-lua): bump version to 0.4.6 2025-12-30 20:23:57 +01:00
7cccd3b512 chore(plugins): bump all plugins to 0.4.6 2025-12-30 20:23:48 +01:00
9f6d0c5935 chore: bump version to 0.4.6 2025-12-30 20:23:38 +01:00
026a232e0c docs: add ROADMAP.md with feature ideas
- High value/low effort: hot-reload, frecency pruning, :recent, clipboard images
- Medium effort: universal actions, plugin settings UI, result capture
- Bigger bets: window switcher, cross-device sync, natural language, plugin marketplace
- Technical debt: meval→evalexpr, API compat, per-plugin config

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 09:04:55 +01:00
1557119448 docs: comprehensive documentation update
README.md:
- Fix bundle package names (add meta- prefix)
- Add Firefox support to bookmarks plugin description
- Add system paths table (plugins, runtimes, example config)
- Add Quick Start section for copying example config
- Expand config example with providers section

docs/PLUGINS.md:
- Add Firefox support to bookmarks
- Fix bundle package names
- Remove outdated [plugins.weather] and [plugins.pomodoro] config examples

docs/PLUGIN_DEVELOPMENT.md:
- Fix Rust edition from 2024 to 2021
- Add position and priority fields to ProviderInfo
- Add ProviderPosition enum documentation

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-30 08:49:30 +01:00
b814d07382 chore(owlry-rune): bump version to 0.4.5 2025-12-30 08:29:57 +01:00
0dead603ec chore(owlry-lua): bump version to 0.4.5 2025-12-30 08:29:57 +01:00
c1eb5ae2eb chore(plugins): bump all plugins to 0.4.5 2025-12-30 08:29:47 +01:00
07847c76d8 chore: bump version to 0.4.5 2025-12-30 08:29:25 +01:00
2dfce67f3b chore(owlry-rune): bump version to 0.4.4 2025-12-30 08:01:52 +01:00
b1198f4600 chore(owlry-lua): bump version to 0.4.4 2025-12-30 08:01:51 +01:00
e6776b803c chore(plugins): bump all plugins to 0.4.4 2025-12-30 08:01:15 +01:00
25 changed files with 459 additions and 200 deletions

34
Cargo.lock generated
View File

@@ -2373,7 +2373,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry" name = "owlry"
version = "0.4.4" version = "0.4.7"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap", "clap",
@@ -2402,7 +2402,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-lua" name = "owlry-lua"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"chrono", "chrono",
@@ -2420,7 +2420,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-api" name = "owlry-plugin-api"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"serde", "serde",
@@ -2428,7 +2428,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-bookmarks" name = "owlry-plugin-bookmarks"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2440,7 +2440,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-calculator" name = "owlry-plugin-calculator"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"meval", "meval",
@@ -2449,7 +2449,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-clipboard" name = "owlry-plugin-clipboard"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2457,7 +2457,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-emoji" name = "owlry-plugin-emoji"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2465,7 +2465,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-filesearch" name = "owlry-plugin-filesearch"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2474,7 +2474,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-media" name = "owlry-plugin-media"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2482,7 +2482,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-pomodoro" name = "owlry-plugin-pomodoro"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2494,7 +2494,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-scripts" name = "owlry-plugin-scripts"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2503,7 +2503,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-ssh" name = "owlry-plugin-ssh"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2512,7 +2512,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-system" name = "owlry-plugin-system"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2520,7 +2520,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-systemd" name = "owlry-plugin-systemd"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2528,7 +2528,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-weather" name = "owlry-plugin-weather"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"dirs", "dirs",
@@ -2541,7 +2541,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-plugin-websearch" name = "owlry-plugin-websearch"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"abi_stable", "abi_stable",
"owlry-plugin-api", "owlry-plugin-api",
@@ -2549,7 +2549,7 @@ dependencies = [
[[package]] [[package]]
name = "owlry-rune" name = "owlry-rune"
version = "0.4.3" version = "0.4.7"
dependencies = [ dependencies = [
"chrono", "chrono",
"dirs", "dirs",

114
README.md
View File

@@ -32,10 +32,10 @@ yay -S owlry
yay -S owlry-plugin-calculator owlry-plugin-weather yay -S owlry-plugin-calculator owlry-plugin-weather
# Or install bundles: # Or install bundles:
yay -S owlry-essentials # calculator, system, ssh, scripts, bookmarks yay -S owlry-meta-essentials # calculator, system, ssh, scripts, bookmarks
yay -S owlry-widgets # weather, media, pomodoro yay -S owlry-meta-widgets # weather, media, pomodoro
yay -S owlry-tools # clipboard, emoji, websearch, filesearch, systemd yay -S owlry-meta-tools # clipboard, emoji, websearch, filesearch, systemd
yay -S owlry-full # everything yay -S owlry-meta-full # everything
# For custom Lua/Rune plugins # For custom Lua/Rune plugins
yay -S owlry-lua # Lua 5.4 runtime yay -S owlry-lua # Lua 5.4 runtime
@@ -53,7 +53,7 @@ yay -S owlry-rune # Rune runtime
| `owlry-plugin-clipboard` | History via cliphist | | `owlry-plugin-clipboard` | History via cliphist |
| `owlry-plugin-emoji` | 400+ searchable emoji | | `owlry-plugin-emoji` | 400+ searchable emoji |
| `owlry-plugin-scripts` | User scripts | | `owlry-plugin-scripts` | User scripts |
| `owlry-plugin-bookmarks` | Chrome, Brave, Edge bookmarks | | `owlry-plugin-bookmarks` | Firefox, Chrome, Brave, Edge bookmarks |
| `owlry-plugin-websearch` | Web search (`? query`) | | `owlry-plugin-websearch` | Web search (`? query`) |
| `owlry-plugin-filesearch` | File search (`/ filename`) | | `owlry-plugin-filesearch` | File search (`/ filename`) |
| `owlry-plugin-systemd` | User services with actions | | `owlry-plugin-systemd` | User services with actions |
@@ -99,12 +99,40 @@ sudo cp target/release/libowlry_plugin_*.so /usr/lib/owlry/plugins/
## Usage ## Usage
```bash ```bash
owlry # Launch with defaults owlry # Launch with all providers
owlry --mode app # Applications only owlry -m app # Applications only
owlry --providers app,cmd # Specific providers owlry -m cmd # PATH commands only
owlry --help # Show all options owlry -p app,cmd # Multiple specific providers
owlry -m calc # Calculator plugin only (if installed)
owlry --help # Show all options with examples
``` ```
### dmenu Mode
Owlry is dmenu-compatible. Pipe input for interactive selection:
```bash
# Basic selection
echo -e "Option A\nOption B\nOption C" | owlry -m dmenu
# Select from files
ls ~/Documents | owlry -m dmenu
# Git branch checkout
git branch | owlry -m dmenu --prompt "checkout:" | xargs git checkout
# Kill a process
ps -eo comm | sort -u | owlry -m dmenu --prompt "kill:" | xargs pkill
# Select and open a project
find ~/projects -maxdepth 1 -type d | owlry -m dmenu | xargs code
# Package manager search
pacman -Ssq | owlry -m dmenu --prompt "install:" | xargs sudo pacman -S
```
The `--prompt` flag sets a custom label for the search input.
### Keyboard Shortcuts ### Keyboard Shortcuts
| Key | Action | | Key | Action |
@@ -158,6 +186,21 @@ Owlry follows the [XDG Base Directory Specification](https://specifications.free
| `~/.local/share/owlry/scripts/` | User scripts | | `~/.local/share/owlry/scripts/` | User scripts |
| `~/.local/share/owlry/frecency.json` | Usage history | | `~/.local/share/owlry/frecency.json` | Usage history |
System locations:
| Path | Purpose |
|------|---------|
| `/usr/lib/owlry/plugins/*.so` | Installed native plugins |
| `/usr/lib/owlry/runtimes/*.so` | Lua/Rune script runtimes |
| `/usr/share/doc/owlry/config.example.toml` | Example configuration |
### Quick Start
```bash
# Copy example config
mkdir -p ~/.config/owlry
cp /usr/share/doc/owlry/config.example.toml ~/.config/owlry/config.toml
```
### Example Configuration ### Example Configuration
```toml ```toml
@@ -169,8 +212,8 @@ tabs = ["app", "cmd", "uuctl"]
# launch_wrapper = "uwsm app --" # Auto-detected # launch_wrapper = "uwsm app --" # Auto-detected
[appearance] [appearance]
width = 700 width = 850
height = 500 height = 650
font_size = 14 font_size = 14
border_radius = 12 border_radius = 12
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc. # theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc.
@@ -178,17 +221,18 @@ border_radius = 12
[plugins] [plugins]
disabled = [] # Plugin IDs to disable, e.g., ["emoji", "pomodoro"] disabled = [] # Plugin IDs to disable, e.g., ["emoji", "pomodoro"]
# Per-plugin configuration (new in 0.4.0) [providers]
[plugins.weather] applications = true # .desktop files
provider = "wttr.in" # or: openweathermap, open-meteo commands = true # PATH executables
location = "Berlin" # city name or "lat,lon" frecency = true # Boost frequently used items
# api_key = "..." # Required for OpenWeatherMap frecency_weight = 0.3 # 0.0-1.0
[plugins.pomodoro] # Web search engine: google, duckduckgo, bing, startpage, brave, ecosia
work_mins = 25 # Work session duration search_engine = "duckduckgo"
break_mins = 5 # Break duration
``` ```
See `/usr/share/doc/owlry/config.example.toml` for all options with documentation.
## Plugin System ## Plugin System
Owlry uses a modular plugin architecture. Plugins are loaded from: Owlry uses a modular plugin architecture. Plugins are loaded from:
@@ -205,6 +249,38 @@ Add plugin IDs to the disabled list in your config:
disabled = ["emoji", "pomodoro"] disabled = ["emoji", "pomodoro"]
``` ```
### Plugin Management CLI
```bash
# List installed plugins
owlry plugin list
owlry plugin list --enabled # Only enabled
owlry plugin list --available # Show registry plugins
# Search registry
owlry plugin search "weather"
# Install/remove
owlry plugin install <name> # From registry
owlry plugin install ./my-plugin # From local path
owlry plugin remove <name>
# Enable/disable
owlry plugin enable <name>
owlry plugin disable <name>
# Plugin info
owlry plugin info <name>
owlry plugin commands <name> # List plugin CLI commands
# Create new plugin
owlry plugin create my-plugin # Lua (default)
owlry plugin create my-plugin -r rune # Rune
# Run plugin command
owlry plugin run <plugin-id> <command> [args...]
```
### Creating Custom Plugins ### Creating Custom Plugins
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for: See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:

91
ROADMAP.md Normal file
View File

@@ -0,0 +1,91 @@
# Owlry Roadmap
Feature ideas and future development plans for Owlry.
## High Value, Low Effort
### Plugin hot-reload
Detect `.so` file changes in `/usr/lib/owlry/plugins/` and reload without restarting the launcher. The loader infrastructure already exists.
### Frecency pruning
Add `max_entries` and `max_age_days` config options. Prune old entries on startup to prevent `frecency.json` from growing unbounded.
### `:recent` prefix
Show last N launched items. Data already exists in frecency.json — just needs a provider to surface it.
### Clipboard images
`cliphist` supports images. Extend the clipboard plugin to show image thumbnails in results.
---
## Medium Effort, High Value
### Actions on any result
Generalize the submenu system beyond systemd. Every result type gets contextual actions:
| Provider | Actions |
|----------|---------|
| Applications | Open, Open in terminal, Show .desktop location |
| Files | Open, Open folder, Copy path, Delete |
| SSH | Connect, Copy hostname, Edit config |
| Bookmarks | Open, Copy URL, Open incognito |
| Clipboard | Paste, Delete from history |
This is the difference between a launcher and a command palette.
### Plugin settings UI
A `:settings` provider that lists installed plugins and their configurable options. Edit values inline, writes to `config.toml`.
### Result action capture
Calculator shows `= 5+3 → 8`. Allow pressing Tab or Ctrl+C to copy the result to clipboard instead of "launching" it. Useful for calculator, file paths, URLs.
---
## Bigger Bets
### Window switcher with live thumbnails
A `windows` plugin using Wayland screencopy to show live thumbnails of open windows. Hyprland and Sway expose window lists via IPC. Could replace Alt+Tab.
### Cross-device bookmark sync
Firefox and Chrome sync bookmarks across devices. Parse sync metadata to show "recently added on other devices" or "bookmarks from phone".
### Natural language commands
Parse simple natural language into system commands:
```
"shutdown in 30 minutes" → systemd-run --user --on-active=30m systemctl poweroff
"remind me in 1 hour" → notify-send scheduled via at/systemd timer
"volume 50%" → wpctl set-volume @DEFAULT_AUDIO_SINK@ 0.5
```
Local pattern matching, no AI/cloud required.
### Plugin marketplace
A curated registry of third-party Lua/Rune plugins with one-command install:
```bash
owlry plugin install github-notifications
owlry plugin install todoist
owlry plugin install spotify-controls
```
The script runtimes make this viable without recompiling.
---
## Technical Debt
### Replace meval with evalexpr
`meval` depends on `nom v1.2.4` which will be rejected by future Rust versions. Migrate calculator plugin and Lua runtime to `evalexpr` v13+.
### Plugin API backwards compatibility
When `API_VERSION` increments, provide a compatibility shim so v3 plugins work with v4 core. Prevents ecosystem fragmentation.
### Per-plugin configuration
Current flat `[providers]` config doesn't scale. Design a `[plugins.weather]`, `[plugins.pomodoro]` structure that plugins can declare and the core validates.
---
## Priority
If we had to pick one: **Actions on any result**. It transforms every provider from "search and launch" to "search and do anything". The ROI is massive.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-lua" name = "owlry-lua"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-api" name = "owlry-plugin-api"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-bookmarks" name = "owlry-plugin-bookmarks"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-calculator" name = "owlry-plugin-calculator"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-clipboard" name = "owlry-plugin-clipboard"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-emoji" name = "owlry-plugin-emoji"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-filesearch" name = "owlry-plugin-filesearch"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-media" name = "owlry-plugin-media"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-pomodoro" name = "owlry-plugin-pomodoro"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-scripts" name = "owlry-plugin-scripts"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-ssh" name = "owlry-plugin-ssh"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-system" name = "owlry-plugin-system"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-systemd" name = "owlry-plugin-systemd"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-weather" name = "owlry-plugin-weather"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-plugin-websearch" name = "owlry-plugin-websearch"
version = "0.4.3" version = "0.4.7"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.workspace = true license.workspace = true

View File

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

View File

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

View File

@@ -10,19 +10,55 @@ use crate::providers::ProviderType;
#[command( #[command(
name = "owlry", name = "owlry",
about = "An owl-themed application launcher for Wayland", about = "An owl-themed application launcher for Wayland",
version long_about = "An owl-themed application launcher for Wayland, built with GTK4 and Layer Shell.\n\n\
Owlry provides fuzzy search across applications, commands, and plugins.\n\
Native plugins add features like calculator, clipboard, emoji, weather, and more.",
version,
after_help = "\
EXAMPLES:
owlry Launch with all providers
owlry -m app Applications only
owlry -m cmd PATH commands only
owlry -m dmenu dmenu-compatible mode (reads from stdin)
owlry -p app,cmd Multiple providers
owlry -m calc Calculator plugin only (if installed)
DMENU MODE:
Pipe input to owlry for interactive selection:
echo -e \"Option A\\nOption B\" | owlry -m dmenu
ls | owlry -m dmenu
git branch | owlry -m dmenu --prompt \"checkout:\"
SEARCH PREFIXES:
:app firefox Search applications
:cmd git Search PATH commands
= 5+3 Calculator (requires plugin)
? rust docs Web search (requires plugin)
/ .bashrc File search (requires plugin)
For configuration, see ~/.config/owlry/config.toml
For plugin management, see: owlry plugin --help"
)] )]
pub struct CliArgs { pub struct CliArgs {
/// Start in single-provider mode (app, cmd, uuctl) /// Start in single-provider mode
#[arg(long, short = 'm', value_parser = parse_provider)] ///
/// Core modes: app, cmd, dmenu
/// Plugin modes: calc, clip, emoji, ssh, sys, bm, file, web, uuctl, weather, media, pomodoro
#[arg(long, short = 'm', value_parser = parse_provider, value_name = "MODE")]
pub mode: Option<ProviderType>, pub mode: Option<ProviderType>,
/// Comma-separated list of enabled providers (app,cmd,uuctl) /// Comma-separated list of enabled providers
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)] ///
/// Examples: -p app,cmd or -p app,calc,emoji
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider, value_name = "PROVIDERS")]
pub providers: Option<Vec<ProviderType>>, pub providers: Option<Vec<ProviderType>>,
/// Custom prompt text for the search input (useful for dmenu mode) /// Custom prompt text for the search input
#[arg(long)] ///
/// Useful in dmenu mode to indicate what the user is selecting.
/// Example: --prompt "Select file:"
#[arg(long, value_name = "TEXT")]
pub prompt: Option<String>, pub prompt: Option<String>,
/// Subcommand to run (if any) /// Subcommand to run (if any)

View File

@@ -95,8 +95,10 @@ pub trait Provider: Send {
/// Manages all providers and handles searching /// Manages all providers and handles searching
pub struct ProviderManager { pub struct ProviderManager {
/// Static providers (apps, commands, and native static plugins) /// Core static providers (apps, commands, dmenu)
providers: Vec<Box<dyn Provider>>, providers: Vec<Box<dyn Provider>>,
/// Static native plugin providers (need query() for submenu support)
static_native_providers: Vec<NativeProvider>,
/// Dynamic providers from native plugins (calculator, websearch, filesearch) /// Dynamic providers from native plugins (calculator, websearch, filesearch)
/// These are queried per-keystroke, not cached /// These are queried per-keystroke, not cached
dynamic_providers: Vec<NativeProvider>, dynamic_providers: Vec<NativeProvider>,
@@ -118,6 +120,7 @@ impl ProviderManager {
pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self { pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self {
let mut manager = Self { let mut manager = Self {
providers: Vec::new(), providers: Vec::new(),
static_native_providers: Vec::new(),
dynamic_providers: Vec::new(), dynamic_providers: Vec::new(),
widget_providers: Vec::new(), widget_providers: Vec::new(),
matcher: SkimMatcherV2::default(), matcher: SkimMatcherV2::default(),
@@ -149,9 +152,9 @@ impl ProviderManager {
info!("Registered widget provider: {} ({})", provider.name(), type_id); info!("Registered widget provider: {} ({})", provider.name(), type_id);
manager.widget_providers.push(provider); manager.widget_providers.push(provider);
} else { } else {
// Static providers with Normal position // Static native providers (keep as NativeProvider for query/submenu support)
info!("Registered static provider: {} ({})", provider.name(), type_id); info!("Registered static provider: {} ({})", provider.name(), type_id);
manager.providers.push(Box::new(provider)); manager.static_native_providers.push(provider);
} }
} }
} }
@@ -170,7 +173,7 @@ impl ProviderManager {
} }
pub fn refresh_all(&mut self) { pub fn refresh_all(&mut self) {
// Refresh static providers (fast, local operations) // Refresh core providers (apps, commands)
for provider in &mut self.providers { for provider in &mut self.providers {
provider.refresh(); provider.refresh();
info!( info!(
@@ -180,6 +183,16 @@ impl ProviderManager {
); );
} }
// Refresh static native providers (clipboard, emoji, ssh, etc.)
for provider in &mut self.static_native_providers {
provider.refresh();
info!(
"Static provider '{}' loaded {} items",
provider.name(),
provider.items().len()
);
}
// Widget providers are refreshed separately to avoid blocking startup // Widget providers are refreshed separately to avoid blocking startup
// Call refresh_widgets() after window is shown // Call refresh_widgets() after window is shown
@@ -201,9 +214,13 @@ impl ProviderManager {
} }
/// Find a native provider by type ID /// Find a native provider by type ID
/// Searches in widget providers and dynamic providers /// Searches in all native provider lists (static, dynamic, widget)
pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> { pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> {
// Check widget providers first (pomodoro, weather, media) // Check static native providers first (clipboard, emoji, ssh, systemd, etc.)
if let Some(p) = self.static_native_providers.iter().find(|p| p.type_id() == type_id) {
return Some(p);
}
// Check widget providers (pomodoro, weather, media)
if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) { if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) {
return Some(p); return Some(p);
} }
@@ -246,37 +263,40 @@ impl ProviderManager {
} }
} }
/// Iterate over all static provider items (core + native static plugins)
fn all_static_items(&self) -> impl Iterator<Item = &LaunchItem> {
self.providers
.iter()
.flat_map(|p| p.items().iter())
.chain(self.static_native_providers.iter().flat_map(|p| p.items().iter()))
}
#[allow(dead_code)] #[allow(dead_code)]
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> { pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
if query.is_empty() { if query.is_empty() {
// Return recent/popular items when query is empty // Return recent/popular items when query is empty
return self.providers return self.all_static_items()
.iter()
.flat_map(|p| p.items().iter().cloned())
.take(max_results) .take(max_results)
.map(|item| (item, 0)) .map(|item| (item.clone(), 0))
.collect(); .collect();
} }
let mut results: Vec<(LaunchItem, i64)> = self.providers let mut results: Vec<(LaunchItem, i64)> = self.all_static_items()
.iter() .filter_map(|item| {
.flat_map(|provider| { // Match against name and description
provider.items().iter().filter_map(|item| { let name_score = self.matcher.fuzzy_match(&item.name, query);
// Match against name and description let desc_score = item.description
let name_score = self.matcher.fuzzy_match(&item.name, query); .as_ref()
let desc_score = item.description .and_then(|d| self.matcher.fuzzy_match(d, query));
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let score = match (name_score, desc_score) { let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)), (Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n), (Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2), // Lower weight for description matches (None, Some(d)) => Some(d / 2), // Lower weight for description matches
(None, None) => None, (None, None) => None,
}; };
score.map(|s| (item.clone(), s)) score.map(|s| (item.clone(), s))
})
}) })
.collect(); .collect();
@@ -293,38 +313,45 @@ impl ProviderManager {
max_results: usize, max_results: usize,
filter: &crate::filter::ProviderFilter, filter: &crate::filter::ProviderFilter,
) -> Vec<(LaunchItem, i64)> { ) -> Vec<(LaunchItem, i64)> {
// Collect items from core providers
let core_items = self
.providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned());
// Collect items from static native providers
let native_items = self
.static_native_providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned());
if query.is_empty() { if query.is_empty() {
return self return core_items
.providers .chain(native_items)
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned())
.take(max_results) .take(max_results)
.map(|item| (item, 0)) .map(|item| (item, 0))
.collect(); .collect();
} }
let mut results: Vec<(LaunchItem, i64)> = self let mut results: Vec<(LaunchItem, i64)> = core_items
.providers .chain(native_items)
.iter() .filter_map(|item| {
.filter(|provider| filter.is_active(provider.provider_type())) let name_score = self.matcher.fuzzy_match(&item.name, query);
.flat_map(|provider| { let desc_score = item
provider.items().iter().filter_map(|item| { .description
let name_score = self.matcher.fuzzy_match(&item.name, query); .as_ref()
let desc_score = item .and_then(|d| self.matcher.fuzzy_match(d, query));
.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
let score = match (name_score, desc_score) { let score = match (name_score, desc_score) {
(Some(n), Some(d)) => Some(n.max(d)), (Some(n), Some(d)) => Some(n.max(d)),
(Some(n), None) => Some(n), (Some(n), None) => Some(n),
(None, Some(d)) => Some(d / 2), (None, Some(d)) => Some(d / 2),
(None, None) => None, (None, None) => None,
}; };
score.map(|s| (item.clone(), s)) score.map(|s| (item, s))
})
}) })
.collect(); .collect();
@@ -384,11 +411,22 @@ impl ProviderManager {
// Empty query (after checking special providers) - return frecency-sorted items // Empty query (after checking special providers) - return frecency-sorted items
if query.is_empty() { if query.is_empty() {
let items: Vec<(LaunchItem, i64)> = self // Collect items from core providers
let core_items = self
.providers .providers
.iter() .iter()
.filter(|p| filter.is_active(p.provider_type())) .filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned()) .flat_map(|p| p.items().iter().cloned());
// Collect items from static native providers
let native_items = self
.static_native_providers
.iter()
.filter(|p| filter.is_active(p.provider_type()))
.flat_map(|p| p.items().iter().cloned());
let items: Vec<(LaunchItem, i64)> = core_items
.chain(native_items)
.filter(|item| { .filter(|item| {
// Apply tag filter if present // Apply tag filter if present
if let Some(tag) = tag_filter { if let Some(tag) = tag_filter {
@@ -412,53 +450,70 @@ impl ProviderManager {
} }
// Regular search with frecency boost and tag matching // Regular search with frecency boost and tag matching
let search_results: Vec<(LaunchItem, i64)> = self // Helper closure for scoring items
.providers let score_item = |item: &LaunchItem| -> Option<(LaunchItem, i64)> {
.iter() // Apply tag filter if present
.filter(|provider| filter.is_active(provider.provider_type())) if let Some(tag) = tag_filter
.flat_map(|provider| { && !item.tags.iter().any(|t| t.to_lowercase().contains(tag))
provider.items().iter().filter_map(|item| { {
// Apply tag filter if present return None;
if let Some(tag) = tag_filter }
&& !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) {
return None;
}
let name_score = self.matcher.fuzzy_match(&item.name, query); let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item let desc_score = item
.description .description
.as_ref() .as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query)); .and_then(|d| self.matcher.fuzzy_match(d, query));
// Also match against tags (lower weight) // Also match against tags (lower weight)
let tag_score = item let tag_score = item
.tags .tags
.iter() .iter()
.filter_map(|t| self.matcher.fuzzy_match(t, query)) .filter_map(|t| self.matcher.fuzzy_match(t, query))
.max() .max()
.map(|s| s / 3); // Lower weight for tag matches .map(|s| s / 3); // Lower weight for tag matches
let base_score = match (name_score, desc_score, tag_score) { let base_score = match (name_score, desc_score, tag_score) {
(Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)), (Some(n), Some(d), Some(t)) => Some(n.max(d).max(t)),
(Some(n), Some(d), None) => Some(n.max(d)), (Some(n), Some(d), None) => Some(n.max(d)),
(Some(n), None, Some(t)) => Some(n.max(t)), (Some(n), None, Some(t)) => Some(n.max(t)),
(Some(n), None, None) => Some(n), (Some(n), None, None) => Some(n),
(None, Some(d), Some(t)) => Some((d / 2).max(t)), (None, Some(d), Some(t)) => Some((d / 2).max(t)),
(None, Some(d), None) => Some(d / 2), (None, Some(d), None) => Some(d / 2),
(None, None, Some(t)) => Some(t), (None, None, Some(t)) => Some(t),
(None, None, None) => None, (None, None, None) => None,
}; };
base_score.map(|s| { base_score.map(|s| {
let frecency_score = frecency.get_score(&item.id); let frecency_score = frecency.get_score(&item.id);
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64; let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
(item.clone(), s + frecency_boost) (item.clone(), s + frecency_boost)
})
})
}) })
.collect(); };
results.extend(search_results); // Search core providers
for provider in &self.providers {
if !filter.is_active(provider.provider_type()) {
continue;
}
for item in provider.items() {
if let Some(scored) = score_item(item) {
results.push(scored);
}
}
}
// Search static native providers
for provider in &self.static_native_providers {
if !filter.is_active(provider.provider_type()) {
continue;
}
for item in provider.items() {
if let Some(scored) = score_item(item) {
results.push(scored);
}
}
}
results.sort_by(|a, b| b.1.cmp(&a.1)); results.sort_by(|a, b| b.1.cmp(&a.1));
results.truncate(max_results); results.truncate(max_results);
@@ -479,7 +534,11 @@ impl ProviderManager {
/// Get all available provider types (for UI tabs) /// Get all available provider types (for UI tabs)
#[allow(dead_code)] #[allow(dead_code)]
pub fn available_providers(&self) -> Vec<ProviderType> { pub fn available_providers(&self) -> Vec<ProviderType> {
self.providers.iter().map(|p| p.provider_type()).collect() self.providers
.iter()
.map(|p| p.provider_type())
.chain(self.static_native_providers.iter().map(|p| p.provider_type()))
.collect()
} }
/// Get a widget item by type_id (e.g., "pomodoro", "weather", "media") /// Get a widget item by type_id (e.g., "pomodoro", "weather", "media")
@@ -519,6 +578,16 @@ impl ProviderManager {
plugin_id, submenu_query plugin_id, submenu_query
); );
// Search in static native providers (clipboard, emoji, ssh, systemd, etc.)
for provider in &self.static_native_providers {
if provider.type_id() == plugin_id {
let actions = provider.query(&submenu_query);
if !actions.is_empty() {
return Some((display_name.to_string(), actions));
}
}
}
// Search in dynamic providers // Search in dynamic providers
for provider in &self.dynamic_providers { for provider in &self.dynamic_providers {
if provider.type_id() == plugin_id { if provider.type_id() == plugin_id {
@@ -539,23 +608,6 @@ impl ProviderManager {
} }
} }
// Search in static providers (boxed)
// Note: Static providers don't typically have submenu support,
// but we check for completeness
for provider in &self.providers {
if let ProviderType::Plugin(type_id) = provider.provider_type()
&& type_id == plugin_id
{
// Static providers use the items() method, not query
// Submenu support requires dynamic query capability
#[cfg(feature = "dev-logging")]
debug!(
"[Submenu] Plugin '{}' is static, cannot query for submenu",
plugin_id
);
}
}
#[cfg(feature = "dev-logging")] #[cfg(feature = "dev-logging")]
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id); debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);

View File

@@ -143,9 +143,10 @@ chmod +x ~/.local/share/owlry/scripts/backup.sh
**Prefix:** `:bm` **Prefix:** `:bm`
**Package:** `owlry-plugin-bookmarks` **Package:** `owlry-plugin-bookmarks`
Browser bookmarks from Chromium-based browsers. Browser bookmarks from Firefox and Chromium-based browsers.
**Supported browsers:** **Supported browsers:**
- Firefox (reads places.sqlite)
- Google Chrome - Google Chrome
- Brave - Brave
- Microsoft Edge - Microsoft Edge
@@ -236,13 +237,7 @@ Current weather displayed at the top of results.
- OpenWeatherMap (requires API key) - OpenWeatherMap (requires API key)
- Open-Meteo (no API key required) - Open-Meteo (no API key required)
**Configuration:** **Note:** Weather configuration is currently embedded in the plugin. Future versions will support runtime 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:** **Features:**
- Temperature, condition, humidity, wind speed - Temperature, condition, humidity, wind speed
@@ -274,13 +269,6 @@ MPRIS media player controls.
Pomodoro timer with work/break cycles. 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:** **Features:**
- Configurable work session duration - Configurable work session duration
- Configurable break duration - Configurable break duration
@@ -301,17 +289,17 @@ For convenience, plugins are available in bundle meta-packages:
| Bundle | Plugins | | Bundle | Plugins |
|--------|---------| |--------|---------|
| `owlry-essentials` | calculator, system, ssh, scripts, bookmarks | | `owlry-meta-essentials` | calculator, system, ssh, scripts, bookmarks |
| `owlry-widgets` | weather, media, pomodoro | | `owlry-meta-widgets` | weather, media, pomodoro |
| `owlry-tools` | clipboard, emoji, websearch, filesearch, systemd | | `owlry-meta-tools` | clipboard, emoji, websearch, filesearch, systemd |
| `owlry-full` | All of the above | | `owlry-meta-full` | All of the above |
```bash ```bash
# Install everything # Install everything
yay -S owlry-full yay -S owlry-meta-full
# Or pick a bundle # Or pick a bundle
yay -S owlry-essentials owlry-widgets yay -S owlry-meta-essentials owlry-meta-widgets
``` ```
--- ---

View File

@@ -23,7 +23,7 @@ Edit `Cargo.toml`:
[package] [package]
name = "owlry-plugin-myplugin" name = "owlry-plugin-myplugin"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
@@ -38,7 +38,7 @@ Edit `src/lib.rs`:
use abi_stable::std_types::{ROption, RStr, RString, RVec}; use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{ use owlry_plugin_api::{
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo,
ProviderKind, API_VERSION, ProviderKind, ProviderPosition, API_VERSION,
}; };
extern "C" fn plugin_info() -> PluginInfo { extern "C" fn plugin_info() -> PluginInfo {
@@ -59,6 +59,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
icon: RString::from("application-x-executable"), icon: RString::from("application-x-executable"),
provider_type: ProviderKind::Static, provider_type: ProviderKind::Static,
type_id: RString::from("myplugin"), type_id: RString::from("myplugin"),
position: ProviderPosition::Normal,
priority: 0, // Use frecency-based ordering
}].into() }].into()
} }
@@ -198,12 +200,19 @@ pub struct ProviderInfo {
pub icon: RString, // Default icon name pub icon: RString, // Default icon name
pub provider_type: ProviderKind, // Static or Dynamic pub provider_type: ProviderKind, // Static or Dynamic
pub type_id: RString, // Short ID for badges pub type_id: RString, // Short ID for badges
pub position: ProviderPosition, // Normal or Widget
pub priority: i32, // Result ordering (higher = first)
} }
pub enum ProviderKind { pub enum ProviderKind {
Static, // Items loaded at startup via refresh() Static, // Items loaded at startup via refresh()
Dynamic, // Items computed per-query via query() 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 ### PluginItem

View File

@@ -179,11 +179,18 @@ bump-meta new_version:
done done
echo "Meta-packages bumped to {{new_version}}" echo "Meta-packages bumped to {{new_version}}"
# Bump all non-core crates (plugins + runtimes) to same version # Bump all crates (core + plugins + runtimes) to same version
bump-all new_version: bump-all new_version:
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# Bump plugins # Bump core
toml="crates/owlry/Cargo.toml"
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
if [ "$old" != "{{new_version}}" ]; then
echo "Bumping owlry from $old to {{new_version}}"
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
fi
# Bump plugins (including plugin-api)
for toml in crates/owlry-plugin-*/Cargo.toml; do for toml in crates/owlry-plugin-*/Cargo.toml; do
crate=$(basename $(dirname "$toml")) crate=$(basename $(dirname "$toml"))
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/') old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
@@ -204,9 +211,9 @@ bump-all new_version:
fi fi
done done
cargo check --workspace cargo check --workspace
git add crates/owlry-plugin-*/Cargo.toml crates/owlry-lua/Cargo.toml crates/owlry-rune/Cargo.toml Cargo.lock git add crates/*/Cargo.toml Cargo.lock
git commit -m "chore: bump all plugins and runtimes to {{new_version}}" git commit -m "chore: bump all crates to {{new_version}}"
echo "All plugins and runtimes bumped to {{new_version}}" echo "All crates bumped to {{new_version}}"
# Bump core version (usage: just bump 0.2.0) # Bump core version (usage: just bump 0.2.0)
bump new_version: bump new_version: