Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bf8a31af78 | |||
| e23bdf5cee | |||
| 25c4d40d36 | |||
| b36dd2a438 | |||
| 35a0f580c3 | |||
| 7ed36c58c2 | |||
| 7cccd3b512 | |||
| 9f6d0c5935 | |||
| 026a232e0c | |||
| 1557119448 | |||
| b814d07382 | |||
| 0dead603ec | |||
| c1eb5ae2eb | |||
| 07847c76d8 | |||
| 2dfce67f3b | |||
| b1198f4600 | |||
| e6776b803c |
34
Cargo.lock
generated
34
Cargo.lock
generated
@@ -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
114
README.md
@@ -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
91
ROADMAP.md
Normal 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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
17
justfile
17
justfile
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user