Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c0ea40a393 | |||
| 44f0915ba9 | |||
| a55567b422 | |||
| 707caefadf | |||
| 78895d34b5 | |||
| e6f217f19c | |||
| ff04675417 | |||
| b85f85c4da | |||
| 1aa92ee1e5 | |||
| 9532b3cfde | |||
| 551e5d74ae | |||
| 60eaffb2ab | |||
| 6d8d4a9f89 | |||
| 3ef9398655 | |||
| 46bb4bfb38 | |||
| c8aed5faf5 | |||
| bf8a31af78 | |||
| e23bdf5cee | |||
| 25c4d40d36 | |||
| b36dd2a438 | |||
| 35a0f580c3 | |||
| 7ed36c58c2 | |||
| 7cccd3b512 | |||
| 9f6d0c5935 | |||
| 026a232e0c | |||
| 1557119448 | |||
| b814d07382 | |||
| 0dead603ec | |||
| c1eb5ae2eb | |||
| 07847c76d8 | |||
| 2dfce67f3b | |||
| b1198f4600 | |||
| e6776b803c | |||
| 6e2d60466b | |||
| 8c1cf88474 | |||
| ecaaae39e3 | |||
| 96e9b09a31 | |||
| e053f7d5d5 | |||
| b1f11c076b | |||
| 2d7fb33f30 | |||
| 3b1ff03ff8 | |||
| e1fb63d6c4 | |||
| 33e2f9cb5e | |||
| 6b21602a07 | |||
| 4516865c21 | |||
| 4fbc7fc4c9 | |||
| 536c5c5012 | |||
| abd4df6939 | |||
| 43f7228be2 | |||
| a1b47b8ba0 | |||
| ccce9b8572 | |||
| ffb4c2f127 | |||
| cde599db03 |
2
.gitignore
vendored
@@ -11,3 +11,5 @@ aur/*/*.tar.gz
|
||||
aur/*/*.tar.xz
|
||||
aur/*/*.pkg.tar.*
|
||||
# Keep PKGBUILD and .SRCINFO tracked
|
||||
.SRCINFO
|
||||
aur/
|
||||
|
||||
92
Cargo.lock
generated
@@ -764,6 +764,18 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fallible-iterator"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
|
||||
|
||||
[[package]]
|
||||
name = "fallible-streaming-iterator"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
@@ -1417,12 +1429,30 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
|
||||
[[package]]
|
||||
name = "hashlink"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
|
||||
dependencies = [
|
||||
"hashbrown 0.14.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -1694,7 +1724,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.16.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1807,6 +1837,17 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libsqlite3-sys"
|
||||
version = "0.30.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
@@ -2332,7 +2373,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry"
|
||||
version = "0.4.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -2361,7 +2402,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-lua"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"chrono",
|
||||
@@ -2379,7 +2420,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-api"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"serde",
|
||||
@@ -2387,18 +2428,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-bookmarks"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"dirs",
|
||||
"owlry-plugin-api",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-calculator"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"meval",
|
||||
@@ -2407,7 +2449,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-clipboard"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"owlry-plugin-api",
|
||||
@@ -2415,7 +2457,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-emoji"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"owlry-plugin-api",
|
||||
@@ -2423,7 +2465,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-filesearch"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"dirs",
|
||||
@@ -2432,7 +2474,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-media"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"owlry-plugin-api",
|
||||
@@ -2440,7 +2482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-pomodoro"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"dirs",
|
||||
@@ -2452,7 +2494,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-scripts"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"dirs",
|
||||
@@ -2461,7 +2503,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-ssh"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"dirs",
|
||||
@@ -2470,7 +2512,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-system"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"owlry-plugin-api",
|
||||
@@ -2478,7 +2520,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-systemd"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"owlry-plugin-api",
|
||||
@@ -2486,7 +2528,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-weather"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"dirs",
|
||||
@@ -2499,7 +2541,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-websearch"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"owlry-plugin-api",
|
||||
@@ -2507,7 +2549,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-rune"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
@@ -3043,6 +3085,20 @@ dependencies = [
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rusqlite"
|
||||
version = "0.32.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"fallible-iterator",
|
||||
"fallible-streaming-iterator",
|
||||
"hashlink",
|
||||
"libsqlite3-sys",
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.1"
|
||||
|
||||
123
README.md
@@ -32,10 +32,10 @@ yay -S owlry
|
||||
yay -S owlry-plugin-calculator owlry-plugin-weather
|
||||
|
||||
# Or install bundles:
|
||||
yay -S owlry-essentials # calculator, system, ssh, scripts, bookmarks
|
||||
yay -S owlry-widgets # weather, media, pomodoro
|
||||
yay -S owlry-tools # clipboard, emoji, websearch, filesearch, systemd
|
||||
yay -S owlry-full # everything
|
||||
yay -S owlry-meta-essentials # calculator, system, ssh, scripts, bookmarks
|
||||
yay -S owlry-meta-widgets # weather, media, pomodoro
|
||||
yay -S owlry-meta-tools # clipboard, emoji, websearch, filesearch, systemd
|
||||
yay -S owlry-meta-full # everything
|
||||
|
||||
# For custom Lua/Rune plugins
|
||||
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-emoji` | 400+ searchable emoji |
|
||||
| `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-filesearch` | File search (`/ filename`) |
|
||||
| `owlry-plugin-systemd` | User services with actions |
|
||||
@@ -99,12 +99,45 @@ sudo cp target/release/libowlry_plugin_*.so /usr/lib/owlry/plugins/
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
owlry # Launch with defaults
|
||||
owlry --mode app # Applications only
|
||||
owlry --providers app,cmd # Specific providers
|
||||
owlry --help # Show all options
|
||||
owlry # Launch with all providers
|
||||
owlry -m app # Applications only
|
||||
owlry -m cmd # PATH commands only
|
||||
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 - the selected item is printed to stdout (not executed), so you pipe the output to execute it:
|
||||
|
||||
```bash
|
||||
# Screenshot menu (execute selected command)
|
||||
printf '%s\n' \
|
||||
"grimblast --notify copy screen" \
|
||||
"grimblast --notify copy area" \
|
||||
"grimblast --notify edit screen" \
|
||||
| owlry -m dmenu -p "Screenshot" \
|
||||
| sh
|
||||
|
||||
# Git branch checkout
|
||||
git branch | owlry -m dmenu -p "checkout" | xargs git checkout
|
||||
|
||||
# Kill a process
|
||||
ps -eo comm | sort -u | owlry -m dmenu -p "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 -p "install" | xargs sudo pacman -S
|
||||
|
||||
# Open selected file
|
||||
ls ~/Documents | owlry -m dmenu | xargs xdg-open
|
||||
```
|
||||
|
||||
The `-p` / `--prompt` flag sets a custom label for the search input.
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Key | Action |
|
||||
@@ -158,6 +191,21 @@ Owlry follows the [XDG Base Directory Specification](https://specifications.free
|
||||
| `~/.local/share/owlry/scripts/` | User scripts |
|
||||
| `~/.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
|
||||
|
||||
```toml
|
||||
@@ -165,12 +213,12 @@ Owlry follows the [XDG Base Directory Specification](https://specifications.free
|
||||
show_icons = true
|
||||
max_results = 10
|
||||
tabs = ["app", "cmd", "uuctl"]
|
||||
# terminal_command = "kitty" # Auto-detected
|
||||
# launch_wrapper = "uwsm app --" # Auto-detected
|
||||
# terminal_command = "kitty" # Auto-detected
|
||||
# use_uwsm = false # Enable for systemd session integration
|
||||
|
||||
[appearance]
|
||||
width = 700
|
||||
height = 500
|
||||
width = 850
|
||||
height = 650
|
||||
font_size = 14
|
||||
border_radius = 12
|
||||
# theme = "owl" # Or: catppuccin-mocha, nord, dracula, etc.
|
||||
@@ -178,17 +226,18 @@ border_radius = 12
|
||||
[plugins]
|
||||
disabled = [] # Plugin IDs to disable, e.g., ["emoji", "pomodoro"]
|
||||
|
||||
# Per-plugin configuration (new in 0.4.0)
|
||||
[plugins.weather]
|
||||
provider = "wttr.in" # or: openweathermap, open-meteo
|
||||
location = "Berlin" # city name or "lat,lon"
|
||||
# api_key = "..." # Required for OpenWeatherMap
|
||||
[providers]
|
||||
applications = true # .desktop files
|
||||
commands = true # PATH executables
|
||||
frecency = true # Boost frequently used items
|
||||
frecency_weight = 0.3 # 0.0-1.0
|
||||
|
||||
[plugins.pomodoro]
|
||||
work_mins = 25 # Work session duration
|
||||
break_mins = 5 # Break duration
|
||||
# Web search engine: google, duckduckgo, bing, startpage, brave, ecosia
|
||||
search_engine = "duckduckgo"
|
||||
```
|
||||
|
||||
See `/usr/share/doc/owlry/config.example.toml` for all options with documentation.
|
||||
|
||||
## Plugin System
|
||||
|
||||
Owlry uses a modular plugin architecture. Plugins are loaded from:
|
||||
@@ -205,6 +254,38 @@ Add plugin IDs to the disabled list in your config:
|
||||
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
|
||||
|
||||
See [docs/PLUGIN_DEVELOPMENT.md](docs/PLUGIN_DEVELOPMENT.md) for:
|
||||
|
||||
109
ROADMAP.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# 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
|
||||
|
||||
### Split monorepo for user build efficiency
|
||||
Currently, a small core fix requires all 16 AUR packages to rebuild (same source tarball). Split into 3 repos:
|
||||
|
||||
| Repo | Contents | Versioning |
|
||||
|------|----------|------------|
|
||||
| `owlry` | Core binary | Independent |
|
||||
| `owlry-plugin-api` | ABI interface (crates.io) | Semver, conservative |
|
||||
| `owlry-plugins` | 13 plugins + 2 runtimes | Independent per plugin |
|
||||
|
||||
**Execution order:**
|
||||
1. Publish `owlry-plugin-api` to crates.io
|
||||
2. Update monorepo to use crates.io dependency
|
||||
3. Create `owlry-plugins` repo, move plugins + runtimes
|
||||
4. Slim current repo to core-only
|
||||
5. Update AUR PKGBUILDs with new source URLs
|
||||
|
||||
**Benefit:** Core bugfix = 1 rebuild. Plugin fix = 1 rebuild. Third-party plugins possible via crates.io.
|
||||
|
||||
### 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]
|
||||
name = "owlry-lua"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-api"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -31,7 +31,9 @@ use abi_stable::StableAbi;
|
||||
pub use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
|
||||
/// Current plugin API version - plugins must match this
|
||||
pub const API_VERSION: u32 = 1;
|
||||
/// v2: Added ProviderPosition for widget support
|
||||
/// v3: Added priority field for plugin-declared result ordering
|
||||
pub const API_VERSION: u32 = 3;
|
||||
|
||||
/// Plugin metadata returned by the info function
|
||||
#[repr(C)]
|
||||
@@ -65,6 +67,14 @@ pub struct ProviderInfo {
|
||||
pub provider_type: ProviderKind,
|
||||
/// Short type identifier for UI badges (e.g., "calc", "web")
|
||||
pub type_id: RString,
|
||||
/// Display position (Normal or Widget)
|
||||
pub position: ProviderPosition,
|
||||
/// Priority for result ordering (higher values appear first)
|
||||
/// Suggested ranges:
|
||||
/// - Widgets: 10000-12000
|
||||
/// - Dynamic providers: 7000-10000
|
||||
/// - Static providers: 0-5000 (use 0 for frecency-based ordering)
|
||||
pub priority: i32,
|
||||
}
|
||||
|
||||
/// Provider behavior type
|
||||
@@ -77,6 +87,20 @@ pub enum ProviderKind {
|
||||
Dynamic,
|
||||
}
|
||||
|
||||
/// Provider display position
|
||||
///
|
||||
/// Controls where in the result list this provider's items appear.
|
||||
#[repr(C)]
|
||||
#[derive(StableAbi, Clone, Copy, Debug, PartialEq, Eq, Default)]
|
||||
pub enum ProviderPosition {
|
||||
/// Standard position in results (sorted by score/frecency)
|
||||
#[default]
|
||||
Normal,
|
||||
/// Widget position - appears at top of results when query is empty
|
||||
/// Widgets are always visible regardless of filter settings
|
||||
Widget,
|
||||
}
|
||||
|
||||
/// A single searchable/launchable item returned by providers
|
||||
#[repr(C)]
|
||||
#[derive(StableAbi, Clone, Debug)]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-bookmarks"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
@@ -25,3 +25,7 @@ dirs = "5.0"
|
||||
# For parsing Chrome bookmarks JSON
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# For reading Firefox bookmarks (places.sqlite)
|
||||
# Use bundled SQLite to avoid system library version conflicts
|
||||
rusqlite = { version = "0.32", features = ["bundled"] }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Bookmarks Plugin for Owlry
|
||||
//!
|
||||
//! A static provider that reads browser bookmarks from Chrome/Chromium.
|
||||
//! Firefox support would require the rusqlite crate for reading places.sqlite.
|
||||
//! A static provider that reads browser bookmarks from various browsers.
|
||||
//!
|
||||
//! Supported browsers:
|
||||
//! - Firefox (via places.sqlite using rusqlite with bundled SQLite)
|
||||
//! - Chrome
|
||||
//! - Chromium
|
||||
//! - Brave
|
||||
@@ -11,11 +11,17 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use rusqlite::{Connection, OpenFlags};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
// Plugin metadata
|
||||
const PLUGIN_ID: &str = "bookmarks";
|
||||
@@ -27,17 +33,141 @@ const PLUGIN_DESCRIPTION: &str = "Browser bookmark search";
|
||||
const PROVIDER_ID: &str = "bookmarks";
|
||||
const PROVIDER_NAME: &str = "Bookmarks";
|
||||
const PROVIDER_PREFIX: &str = ":bm";
|
||||
const PROVIDER_ICON: &str = "web-browser";
|
||||
const PROVIDER_ICON: &str = "user-bookmarks-symbolic";
|
||||
const PROVIDER_TYPE_ID: &str = "bookmarks";
|
||||
|
||||
/// Bookmarks provider state - holds cached items
|
||||
struct BookmarksState {
|
||||
/// Cached bookmark items (returned immediately on refresh)
|
||||
items: Vec<PluginItem>,
|
||||
/// Flag to prevent concurrent background loads
|
||||
loading: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl BookmarksState {
|
||||
fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
loading: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or create the favicon cache directory
|
||||
fn favicon_cache_dir() -> Option<PathBuf> {
|
||||
dirs::cache_dir().map(|d| d.join("owlry/favicons"))
|
||||
}
|
||||
|
||||
/// Ensure the favicon cache directory exists
|
||||
fn ensure_favicon_cache_dir() -> Option<PathBuf> {
|
||||
Self::favicon_cache_dir().and_then(|dir| {
|
||||
fs::create_dir_all(&dir).ok()?;
|
||||
Some(dir)
|
||||
})
|
||||
}
|
||||
|
||||
/// Hash a URL to create a cache filename
|
||||
fn url_to_cache_filename(url: &str) -> String {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
url.hash(&mut hasher);
|
||||
format!("{:016x}.png", hasher.finish())
|
||||
}
|
||||
|
||||
/// Get the bookmark cache file path
|
||||
fn bookmark_cache_file() -> Option<PathBuf> {
|
||||
dirs::cache_dir().map(|d| d.join("owlry/bookmarks.json"))
|
||||
}
|
||||
|
||||
/// Load cached bookmarks from disk (fast)
|
||||
fn load_cached_bookmarks() -> Vec<PluginItem> {
|
||||
let cache_file = match Self::bookmark_cache_file() {
|
||||
Some(f) => f,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
if !cache_file.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let content = match fs::read_to_string(&cache_file) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
// Parse cached bookmarks (simple JSON format)
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CachedBookmark {
|
||||
id: String,
|
||||
name: String,
|
||||
command: String,
|
||||
description: Option<String>,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
let cached: Vec<CachedBookmark> = match serde_json::from_str(&content) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
cached
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
let mut item = PluginItem::new(b.id, b.name, b.command)
|
||||
.with_icon(&b.icon)
|
||||
.with_keywords(vec!["bookmark".to_string()]);
|
||||
if let Some(desc) = b.description {
|
||||
item = item.with_description(desc);
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Save bookmarks to cache file
|
||||
fn save_cached_bookmarks(items: &[PluginItem]) {
|
||||
let cache_file = match Self::bookmark_cache_file() {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Ensure cache directory exists
|
||||
if let Some(parent) = cache_file.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct CachedBookmark {
|
||||
id: String,
|
||||
name: String,
|
||||
command: String,
|
||||
description: Option<String>,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
let cached: Vec<CachedBookmark> = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let desc: Option<String> = match &item.description {
|
||||
abi_stable::std_types::ROption::RSome(s) => Some(s.to_string()),
|
||||
abi_stable::std_types::ROption::RNone => None,
|
||||
};
|
||||
let icon: String = match &item.icon {
|
||||
abi_stable::std_types::ROption::RSome(s) => s.to_string(),
|
||||
abi_stable::std_types::ROption::RNone => PROVIDER_ICON.to_string(),
|
||||
};
|
||||
CachedBookmark {
|
||||
id: item.id.to_string(),
|
||||
name: item.name.to_string(),
|
||||
command: item.command.to_string(),
|
||||
description: desc,
|
||||
icon,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Ok(json) = serde_json::to_string(&cached) {
|
||||
let _ = fs::write(&cache_file, json);
|
||||
}
|
||||
}
|
||||
|
||||
fn chromium_bookmark_paths() -> Vec<PathBuf> {
|
||||
@@ -61,18 +191,77 @@ impl BookmarksState {
|
||||
paths
|
||||
}
|
||||
|
||||
fn load_bookmarks(&mut self) {
|
||||
self.items.clear();
|
||||
fn firefox_places_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
// Load Chrome/Chromium bookmarks
|
||||
for path in Self::chromium_bookmark_paths() {
|
||||
if path.exists() {
|
||||
self.read_chrome_bookmarks(&path);
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let firefox_dir = home.join(".mozilla/firefox");
|
||||
if firefox_dir.exists() {
|
||||
// Find all profile directories
|
||||
if let Ok(entries) = fs::read_dir(&firefox_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let places = path.join("places.sqlite");
|
||||
if places.exists() {
|
||||
paths.push(places);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
/// Find Firefox favicons.sqlite paths (paired with places.sqlite)
|
||||
fn firefox_favicons_path(places_path: &Path) -> Option<PathBuf> {
|
||||
let favicons = places_path.parent()?.join("favicons.sqlite");
|
||||
if favicons.exists() {
|
||||
Some(favicons)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn read_chrome_bookmarks(&mut self, path: &PathBuf) {
|
||||
fn load_bookmarks(&mut self) {
|
||||
// Fast path: load from cache immediately
|
||||
if self.items.is_empty() {
|
||||
self.items = Self::load_cached_bookmarks();
|
||||
}
|
||||
|
||||
// Don't start another background load if one is already running
|
||||
if self.loading.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn background thread to refresh bookmarks
|
||||
let loading = self.loading.clone();
|
||||
thread::spawn(move || {
|
||||
let mut items = Vec::new();
|
||||
|
||||
// Load Chrome/Chromium bookmarks (fast - just JSON parsing)
|
||||
for path in Self::chromium_bookmark_paths() {
|
||||
if path.exists() {
|
||||
Self::read_chrome_bookmarks_static(&path, &mut items);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Firefox bookmarks with favicons (synchronous with rusqlite)
|
||||
for path in Self::firefox_places_paths() {
|
||||
Self::read_firefox_bookmarks(&path, &mut items);
|
||||
}
|
||||
|
||||
// Save to cache for next startup
|
||||
Self::save_cached_bookmarks(&items);
|
||||
|
||||
loading.store(false, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
/// Read Chrome bookmarks (static helper for background thread)
|
||||
fn read_chrome_bookmarks_static(path: &PathBuf, items: &mut Vec<PluginItem>) {
|
||||
let content = match fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
@@ -83,29 +272,27 @@ impl BookmarksState {
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// Process bookmark bar and other folders
|
||||
if let Some(roots) = bookmarks.roots {
|
||||
if let Some(bar) = roots.bookmark_bar {
|
||||
self.process_chrome_folder(&bar);
|
||||
Self::process_chrome_folder_static(&bar, items);
|
||||
}
|
||||
if let Some(other) = roots.other {
|
||||
self.process_chrome_folder(&other);
|
||||
Self::process_chrome_folder_static(&other, items);
|
||||
}
|
||||
if let Some(synced) = roots.synced {
|
||||
self.process_chrome_folder(&synced);
|
||||
Self::process_chrome_folder_static(&synced, items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_chrome_folder(&mut self, folder: &ChromeBookmarkNode) {
|
||||
fn process_chrome_folder_static(folder: &ChromeBookmarkNode, items: &mut Vec<PluginItem>) {
|
||||
if let Some(ref children) = folder.children {
|
||||
for child in children {
|
||||
match child.node_type.as_deref() {
|
||||
Some("url") => {
|
||||
if let Some(ref url) = child.url {
|
||||
let name = child.name.clone().unwrap_or_else(|| url.clone());
|
||||
|
||||
self.items.push(
|
||||
items.push(
|
||||
PluginItem::new(
|
||||
format!("bookmark:{}", url),
|
||||
name,
|
||||
@@ -113,19 +300,183 @@ impl BookmarksState {
|
||||
)
|
||||
.with_description(url.clone())
|
||||
.with_icon(PROVIDER_ICON)
|
||||
.with_keywords(vec!["bookmark".to_string(), "web".to_string()]),
|
||||
.with_keywords(vec!["bookmark".to_string(), "chrome".to_string()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("folder") => {
|
||||
// Recursively process subfolders
|
||||
self.process_chrome_folder(child);
|
||||
Self::process_chrome_folder_static(child, items);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read Firefox bookmarks using rusqlite (synchronous, bundled SQLite)
|
||||
fn read_firefox_bookmarks(places_path: &PathBuf, items: &mut Vec<PluginItem>) {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let temp_db = temp_dir.join("owlry_places_temp.sqlite");
|
||||
|
||||
// Copy database to temp location to avoid locking issues
|
||||
if fs::copy(places_path, &temp_db).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Also copy WAL file if it exists
|
||||
let wal_path = places_path.with_extension("sqlite-wal");
|
||||
if wal_path.exists() {
|
||||
let temp_wal = temp_db.with_extension("sqlite-wal");
|
||||
let _ = fs::copy(&wal_path, &temp_wal);
|
||||
}
|
||||
|
||||
// Copy favicons database if available
|
||||
let favicons_path = Self::firefox_favicons_path(places_path);
|
||||
let temp_favicons = temp_dir.join("owlry_favicons_temp.sqlite");
|
||||
if let Some(ref fp) = favicons_path {
|
||||
let _ = fs::copy(fp, &temp_favicons);
|
||||
let fav_wal = fp.with_extension("sqlite-wal");
|
||||
if fav_wal.exists() {
|
||||
let _ = fs::copy(&fav_wal, temp_favicons.with_extension("sqlite-wal"));
|
||||
}
|
||||
}
|
||||
|
||||
let cache_dir = Self::ensure_favicon_cache_dir();
|
||||
|
||||
// Read bookmarks from places.sqlite
|
||||
let bookmarks = Self::fetch_firefox_bookmarks(&temp_db, &temp_favicons, cache_dir.as_ref());
|
||||
|
||||
// Clean up temp files
|
||||
let _ = fs::remove_file(&temp_db);
|
||||
let _ = fs::remove_file(temp_db.with_extension("sqlite-wal"));
|
||||
let _ = fs::remove_file(&temp_favicons);
|
||||
let _ = fs::remove_file(temp_favicons.with_extension("sqlite-wal"));
|
||||
|
||||
for (title, url, favicon_path) in bookmarks {
|
||||
let icon = favicon_path.unwrap_or_else(|| PROVIDER_ICON.to_string());
|
||||
items.push(
|
||||
PluginItem::new(
|
||||
format!("bookmark:firefox:{}", url),
|
||||
title,
|
||||
format!("xdg-open '{}'", url.replace('\'', "'\\''")),
|
||||
)
|
||||
.with_description(url)
|
||||
.with_icon(&icon)
|
||||
.with_keywords(vec!["bookmark".to_string(), "firefox".to_string()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch Firefox bookmarks with optional favicons
|
||||
fn fetch_firefox_bookmarks(
|
||||
places_path: &Path,
|
||||
favicons_path: &Path,
|
||||
cache_dir: Option<&PathBuf>,
|
||||
) -> Vec<(String, String, Option<String>)> {
|
||||
// Open places.sqlite in read-only mode
|
||||
let conn = match Connection::open_with_flags(
|
||||
places_path,
|
||||
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
// Query bookmarks joining moz_bookmarks with moz_places
|
||||
// type=1 means URL bookmarks (not folders, separators, etc.)
|
||||
let query = r#"
|
||||
SELECT b.title, p.url
|
||||
FROM moz_bookmarks b
|
||||
JOIN moz_places p ON b.fk = p.id
|
||||
WHERE b.type = 1
|
||||
AND p.url NOT LIKE 'place:%'
|
||||
AND p.url NOT LIKE 'about:%'
|
||||
AND b.title IS NOT NULL
|
||||
AND b.title != ''
|
||||
ORDER BY b.dateAdded DESC
|
||||
LIMIT 500
|
||||
"#;
|
||||
|
||||
let mut stmt = match conn.prepare(query) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let bookmarks: Vec<(String, String)> = stmt
|
||||
.query_map([], |row| {
|
||||
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
|
||||
})
|
||||
.ok()
|
||||
.map(|rows| rows.filter_map(|r| r.ok()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// If no favicons or cache dir, return without favicons
|
||||
let cache_dir = match cache_dir {
|
||||
Some(c) => c,
|
||||
None => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(),
|
||||
};
|
||||
|
||||
// Try to open favicons database
|
||||
let fav_conn = match Connection::open_with_flags(
|
||||
favicons_path,
|
||||
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||
) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(),
|
||||
};
|
||||
|
||||
// Fetch favicons for each URL
|
||||
let mut results = Vec::new();
|
||||
for (title, url) in bookmarks {
|
||||
let favicon_path = Self::get_favicon_for_url(&fav_conn, &url, cache_dir);
|
||||
results.push((title, url, favicon_path));
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Get favicon for a URL, caching to file if needed
|
||||
fn get_favicon_for_url(
|
||||
conn: &Connection,
|
||||
page_url: &str,
|
||||
cache_dir: &Path,
|
||||
) -> Option<String> {
|
||||
// Check if already cached
|
||||
let cache_filename = Self::url_to_cache_filename(page_url);
|
||||
let cache_path = cache_dir.join(&cache_filename);
|
||||
if cache_path.exists() {
|
||||
return Some(cache_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Query favicon data from database
|
||||
// Join moz_pages_w_icons -> moz_icons_to_pages -> moz_icons
|
||||
// Prefer smaller icons (32px) for efficiency
|
||||
let query = r#"
|
||||
SELECT i.data
|
||||
FROM moz_pages_w_icons p
|
||||
JOIN moz_icons_to_pages ip ON p.id = ip.page_id
|
||||
JOIN moz_icons i ON ip.icon_id = i.id
|
||||
WHERE p.page_url = ?
|
||||
AND i.data IS NOT NULL
|
||||
ORDER BY ABS(i.width - 32) ASC
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
let data: Option<Vec<u8>> = conn
|
||||
.query_row(query, [page_url], |row| row.get(0))
|
||||
.ok();
|
||||
|
||||
let data = data?;
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Write favicon data to cache file
|
||||
let mut file = fs::File::create(&cache_path).ok()?;
|
||||
file.write_all(&data).ok()?;
|
||||
|
||||
Some(cache_path.to_string_lossy().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome bookmark JSON structures
|
||||
@@ -172,6 +523,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Static: use frecency ordering
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
@@ -241,6 +594,14 @@ mod tests {
|
||||
assert!(!paths.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_firefox_paths() {
|
||||
// This will find paths if Firefox is installed
|
||||
let paths = BookmarksState::firefox_places_paths();
|
||||
// Path detection should work (may be empty if Firefox not installed)
|
||||
let _ = paths.len(); // Just ensure it doesn't panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_chrome_bookmarks() {
|
||||
let json = r#"{
|
||||
@@ -271,7 +632,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_process_folder() {
|
||||
let mut state = BookmarksState::new();
|
||||
let mut items = Vec::new();
|
||||
|
||||
let folder = ChromeBookmarkNode {
|
||||
name: Some("Test Folder".to_string()),
|
||||
@@ -287,9 +648,9 @@ mod tests {
|
||||
]),
|
||||
};
|
||||
|
||||
state.process_chrome_folder(&folder);
|
||||
assert_eq!(state.items.len(), 1);
|
||||
assert_eq!(state.items[0].name.as_str(), "Test Bookmark");
|
||||
BookmarksState::process_chrome_folder_static(&folder, &mut items);
|
||||
assert_eq!(items.len(), 1);
|
||||
assert_eq!(items[0].name.as_str(), "Test Bookmark");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-calculator"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
|
||||
// Plugin metadata
|
||||
@@ -51,6 +52,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Dynamic,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 10000, // Dynamic: calculator results first
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-clipboard"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use std::process::Command;
|
||||
|
||||
@@ -137,6 +138,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Static: use frecency ordering
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-emoji"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
|
||||
// Plugin metadata
|
||||
@@ -423,6 +424,7 @@ impl EmojiState {
|
||||
name.to_string(),
|
||||
format!("printf '%s' '{}' | wl-copy", emoji),
|
||||
)
|
||||
.with_icon(*emoji) // Use emoji character as icon
|
||||
.with_description(format!("{} {}", emoji, keywords))
|
||||
.with_keywords(vec![name.to_string(), keywords.to_string()]),
|
||||
);
|
||||
@@ -452,6 +454,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Static: use frecency ordering
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-filesearch"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
@@ -207,6 +208,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Dynamic,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 8000, // Dynamic: file search
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-media"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use std::process::Command;
|
||||
|
||||
@@ -355,6 +356,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Widget,
|
||||
priority: 11000, // Widget: media player
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-pomodoro"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
notify_with_urgency, owlry_plugin, NotifyUrgency, PluginInfo, PluginItem, ProviderHandle,
|
||||
ProviderInfo, ProviderKind, API_VERSION,
|
||||
ProviderInfo, ProviderKind, ProviderPosition, API_VERSION,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
@@ -57,31 +57,31 @@ impl PomodoroConfig {
|
||||
let config_content = config_path
|
||||
.and_then(|p| fs::read_to_string(p).ok());
|
||||
|
||||
if let Some(content) = config_content {
|
||||
if let Ok(toml) = content.parse::<toml::Table>() {
|
||||
// Try [plugins.pomodoro] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) {
|
||||
if let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table()) {
|
||||
return Self::from_toml_table(pomodoro);
|
||||
}
|
||||
}
|
||||
if let Some(content) = config_content
|
||||
&& let Ok(toml) = content.parse::<toml::Table>()
|
||||
{
|
||||
// Try [plugins.pomodoro] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table())
|
||||
&& let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table())
|
||||
{
|
||||
return Self::from_toml_table(pomodoro);
|
||||
}
|
||||
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let work_mins = providers
|
||||
.get("pomodoro_work_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_WORK_MINS);
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let work_mins = providers
|
||||
.get("pomodoro_work_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_WORK_MINS);
|
||||
|
||||
let break_mins = providers
|
||||
.get("pomodoro_break_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_BREAK_MINS);
|
||||
let break_mins = providers
|
||||
.get("pomodoro_break_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_BREAK_MINS);
|
||||
|
||||
return Self { work_mins, break_mins };
|
||||
}
|
||||
return Self { work_mins, break_mins };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,6 +396,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Widget,
|
||||
priority: 11500, // Widget: pomodoro timer
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-scripts"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use std::fs;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
@@ -187,6 +188,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Static: use frecency ordering
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-ssh"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -9,7 +9,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
@@ -204,6 +205,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Static: use frecency ordering
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-system"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
|
||||
// Plugin metadata
|
||||
@@ -129,6 +130,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Static: use frecency ordering
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-systemd"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use std::process::Command;
|
||||
|
||||
@@ -285,6 +286,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Static: use frecency ordering
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-weather"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
@@ -82,41 +83,41 @@ impl WeatherConfig {
|
||||
let config_content = config_path
|
||||
.and_then(|p| fs::read_to_string(p).ok());
|
||||
|
||||
if let Some(content) = config_content {
|
||||
if let Ok(toml) = content.parse::<toml::Table>() {
|
||||
// Try [plugins.weather] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) {
|
||||
if let Some(weather) = plugins.get("weather").and_then(|v| v.as_table()) {
|
||||
return Self::from_toml_table(weather);
|
||||
}
|
||||
}
|
||||
if let Some(content) = config_content
|
||||
&& let Ok(toml) = content.parse::<toml::Table>()
|
||||
{
|
||||
// Try [plugins.weather] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table())
|
||||
&& let Some(weather) = plugins.get("weather").and_then(|v| v.as_table())
|
||||
{
|
||||
return Self::from_toml_table(weather);
|
||||
}
|
||||
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let provider_str = providers
|
||||
.get("weather_provider")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("wttr.in");
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let provider_str = providers
|
||||
.get("weather_provider")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("wttr.in");
|
||||
|
||||
let provider = provider_str.parse().unwrap_or(WeatherProviderType::WttrIn);
|
||||
let provider = provider_str.parse().unwrap_or(WeatherProviderType::WttrIn);
|
||||
|
||||
let api_key = providers
|
||||
.get("weather_api_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
let api_key = providers
|
||||
.get("weather_api_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
|
||||
let location = providers
|
||||
.get("weather_location")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let location = providers
|
||||
.get("weather_location")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
return Self {
|
||||
provider,
|
||||
api_key,
|
||||
location,
|
||||
};
|
||||
}
|
||||
return Self {
|
||||
provider,
|
||||
api_key,
|
||||
location,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,6 +643,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Widget,
|
||||
priority: 12000, // Widget: highest priority
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-websearch"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -10,7 +10,8 @@
|
||||
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
ProviderPosition, API_VERSION,
|
||||
};
|
||||
|
||||
// Plugin metadata
|
||||
@@ -164,6 +165,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from(PROVIDER_ICON),
|
||||
provider_type: ProviderKind::Dynamic,
|
||||
type_id: RString::from(PROVIDER_TYPE_ID),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 9000, // Dynamic: web search
|
||||
}]
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-rune"
|
||||
version = "0.2.0"
|
||||
version = "0.4.10"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "Rune scripting runtime for owlry plugins"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry"
|
||||
version = "0.4.0"
|
||||
version = "0.4.10"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||
|
||||
@@ -56,7 +56,10 @@ impl OwlryApp {
|
||||
let native_providers = Self::load_native_plugins(&config.borrow());
|
||||
|
||||
// Create provider manager with native plugins
|
||||
#[cfg(feature = "lua")]
|
||||
let mut provider_manager = ProviderManager::with_native_plugins(native_providers);
|
||||
#[cfg(not(feature = "lua"))]
|
||||
let provider_manager = ProviderManager::with_native_plugins(native_providers);
|
||||
|
||||
// Load Lua plugins if enabled (requires lua feature)
|
||||
#[cfg(feature = "lua")]
|
||||
@@ -75,7 +78,7 @@ impl OwlryApp {
|
||||
);
|
||||
let filter = Rc::new(RefCell::new(filter));
|
||||
|
||||
let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone());
|
||||
let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone(), args.prompt.clone());
|
||||
|
||||
// Set up layer shell for Wayland overlay behavior
|
||||
window.init_layer_shell();
|
||||
|
||||
@@ -10,17 +10,57 @@ use crate::providers::ProviderType;
|
||||
#[command(
|
||||
name = "owlry",
|
||||
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 {
|
||||
/// Start in single-provider mode (app, cmd, uuctl)
|
||||
#[arg(long, short = 'm', value_parser = parse_provider)]
|
||||
/// Start in single-provider mode
|
||||
///
|
||||
/// 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>,
|
||||
|
||||
/// Comma-separated list of enabled providers (app,cmd,uuctl)
|
||||
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)]
|
||||
/// Comma-separated list of enabled providers
|
||||
///
|
||||
/// 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>>,
|
||||
|
||||
/// Custom prompt text for the search input
|
||||
///
|
||||
/// Useful in dmenu mode to indicate what the user is selecting.
|
||||
/// Example: --prompt "Select file:"
|
||||
#[arg(long, value_name = "TEXT")]
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Subcommand to run (if any)
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
@@ -6,10 +6,13 @@ use std::process::Command;
|
||||
|
||||
use crate::paths;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub general: GeneralConfig,
|
||||
#[serde(default)]
|
||||
pub appearance: AppearanceConfig,
|
||||
#[serde(default)]
|
||||
pub providers: ProvidersConfig,
|
||||
#[serde(default)]
|
||||
pub plugins: PluginsConfig,
|
||||
@@ -17,20 +20,41 @@ pub struct Config {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneralConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub show_icons: bool,
|
||||
#[serde(default = "default_max_results")]
|
||||
pub max_results: usize,
|
||||
pub terminal_command: String,
|
||||
/// Launch wrapper command for app execution.
|
||||
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --"
|
||||
/// If None or empty, launches directly via sh -c
|
||||
/// Terminal command (auto-detected if not specified)
|
||||
#[serde(default)]
|
||||
pub launch_wrapper: Option<String>,
|
||||
pub terminal_command: Option<String>,
|
||||
/// Enable uwsm (Universal Wayland Session Manager) for launching apps.
|
||||
/// When enabled, desktop files are launched via `uwsm app -- <file>`
|
||||
/// which starts apps in a proper systemd user session.
|
||||
/// When disabled (default), apps are launched via `gio launch`.
|
||||
#[serde(default)]
|
||||
pub use_uwsm: bool,
|
||||
/// Provider tabs shown in the header bar.
|
||||
/// Valid values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
||||
#[serde(default = "default_tabs")]
|
||||
pub tabs: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for GeneralConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_icons: true,
|
||||
max_results: 100,
|
||||
terminal_command: None,
|
||||
use_uwsm: false,
|
||||
tabs: default_tabs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_max_results() -> usize {
|
||||
100
|
||||
}
|
||||
|
||||
fn default_tabs() -> Vec<String> {
|
||||
vec![
|
||||
"app".to_string(),
|
||||
@@ -40,9 +64,10 @@ fn default_tabs() -> Vec<String> {
|
||||
}
|
||||
|
||||
/// User-customizable theme colors
|
||||
/// All fields are optional - unset values inherit from GTK theme
|
||||
/// All fields are optional - unset values inherit from theme or GTK defaults
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ThemeColors {
|
||||
// Core colors
|
||||
pub background: Option<String>,
|
||||
pub background_secondary: Option<String>,
|
||||
pub border: Option<String>,
|
||||
@@ -64,13 +89,21 @@ pub struct ThemeColors {
|
||||
pub badge_sys: Option<String>,
|
||||
pub badge_uuctl: Option<String>,
|
||||
pub badge_web: Option<String>,
|
||||
// Widget badge colors
|
||||
pub badge_media: Option<String>,
|
||||
pub badge_weather: Option<String>,
|
||||
pub badge_pomo: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppearanceConfig {
|
||||
#[serde(default = "default_width")]
|
||||
pub width: i32,
|
||||
#[serde(default = "default_height")]
|
||||
pub height: i32,
|
||||
#[serde(default = "default_font_size")]
|
||||
pub font_size: u32,
|
||||
#[serde(default = "default_border_radius")]
|
||||
pub border_radius: u32,
|
||||
/// Theme name: None = GTK default, "owl" = built-in owl theme
|
||||
#[serde(default)]
|
||||
@@ -80,10 +113,31 @@ pub struct AppearanceConfig {
|
||||
pub colors: ThemeColors,
|
||||
}
|
||||
|
||||
impl Default for AppearanceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: 850,
|
||||
height: 650,
|
||||
font_size: 14,
|
||||
border_radius: 12,
|
||||
theme: None,
|
||||
colors: ThemeColors::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_width() -> i32 { 850 }
|
||||
fn default_height() -> i32 { 650 }
|
||||
fn default_font_size() -> u32 { 14 }
|
||||
fn default_border_radius() -> u32 { 12 }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProvidersConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub applications: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub commands: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub uuctl: bool,
|
||||
/// Enable calculator provider (= expression or calc expression)
|
||||
#[serde(default = "default_true")]
|
||||
@@ -159,6 +213,36 @@ pub struct ProvidersConfig {
|
||||
pub pomodoro_break_mins: u32,
|
||||
}
|
||||
|
||||
impl Default for ProvidersConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
applications: true,
|
||||
commands: true,
|
||||
uuctl: true,
|
||||
calculator: true,
|
||||
frecency: true,
|
||||
frecency_weight: 0.3,
|
||||
websearch: true,
|
||||
search_engine: "duckduckgo".to_string(),
|
||||
system: true,
|
||||
ssh: true,
|
||||
clipboard: true,
|
||||
bookmarks: true,
|
||||
emoji: true,
|
||||
scripts: true,
|
||||
files: true,
|
||||
media: true,
|
||||
weather: false,
|
||||
weather_provider: "wttr.in".to_string(),
|
||||
weather_api_key: None,
|
||||
weather_location: Some("Berlin".to_string()),
|
||||
pomodoro: false,
|
||||
pomodoro_work_mins: 25,
|
||||
pomodoro_break_mins: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for plugins
|
||||
///
|
||||
/// Supports per-plugin configuration via `[plugins.<name>]` sections:
|
||||
@@ -313,28 +397,6 @@ fn default_pomodoro_break() -> u32 {
|
||||
5
|
||||
}
|
||||
|
||||
/// Detect the best launch wrapper for the current session
|
||||
/// Checks for uwsm (Universal Wayland Session Manager) and hyprland
|
||||
fn detect_launch_wrapper() -> Option<String> {
|
||||
// Check if running under uwsm (has UWSM_FINALIZE_VARNAMES or similar uwsm env vars)
|
||||
if (std::env::var("UWSM_FINALIZE_VARNAMES").is_ok()
|
||||
|| std::env::var("__UWSM_SELECT_TAG").is_ok())
|
||||
&& command_exists("uwsm") {
|
||||
debug!("Detected uwsm session, using 'uwsm app --' wrapper");
|
||||
return Some("uwsm app --".to_string());
|
||||
}
|
||||
|
||||
// Check if running under Hyprland
|
||||
if std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok()
|
||||
&& command_exists("hyprctl") {
|
||||
debug!("Detected Hyprland session, using 'hyprctl dispatch exec --' wrapper");
|
||||
return Some("hyprctl dispatch exec --".to_string());
|
||||
}
|
||||
|
||||
// No wrapper needed for other environments
|
||||
debug!("No launch wrapper detected, using direct execution");
|
||||
None
|
||||
}
|
||||
|
||||
/// Detect the best available terminal emulator
|
||||
/// Fallback chain:
|
||||
@@ -450,57 +512,7 @@ fn command_exists(cmd: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let terminal = detect_terminal();
|
||||
info!("Detected terminal: {}", terminal);
|
||||
|
||||
Self {
|
||||
general: GeneralConfig {
|
||||
show_icons: true,
|
||||
max_results: 10,
|
||||
terminal_command: terminal,
|
||||
launch_wrapper: detect_launch_wrapper(),
|
||||
tabs: default_tabs(),
|
||||
},
|
||||
appearance: AppearanceConfig {
|
||||
width: 850,
|
||||
height: 650,
|
||||
font_size: 14,
|
||||
border_radius: 12,
|
||||
theme: None,
|
||||
colors: ThemeColors::default(),
|
||||
},
|
||||
providers: ProvidersConfig {
|
||||
applications: true,
|
||||
commands: true,
|
||||
uuctl: true,
|
||||
calculator: true,
|
||||
frecency: true,
|
||||
frecency_weight: 0.3,
|
||||
websearch: true,
|
||||
search_engine: "duckduckgo".to_string(),
|
||||
system: true,
|
||||
ssh: true,
|
||||
clipboard: true,
|
||||
bookmarks: true,
|
||||
emoji: true,
|
||||
scripts: true,
|
||||
files: true,
|
||||
// Widget providers
|
||||
media: true,
|
||||
weather: false,
|
||||
weather_provider: "wttr.in".to_string(),
|
||||
weather_api_key: None,
|
||||
weather_location: Some("Berlin".to_string()),
|
||||
pomodoro: false,
|
||||
pomodoro_work_mins: 25,
|
||||
pomodoro_break_mins: 5,
|
||||
},
|
||||
plugins: PluginsConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: Config derives Default via #[derive(Default)] - all sub-structs have impl Default
|
||||
|
||||
impl Config {
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
@@ -517,23 +529,32 @@ impl Config {
|
||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
||||
|
||||
if !path.exists() {
|
||||
let mut config = if !path.exists() {
|
||||
info!("Config file not found, using defaults");
|
||||
return Ok(Self::default());
|
||||
}
|
||||
Self::default()
|
||||
} else {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
info!("Loaded config from {:?}", path);
|
||||
config
|
||||
};
|
||||
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
let mut config: Config = toml::from_str(&content)?;
|
||||
info!("Loaded config from {:?}", path);
|
||||
|
||||
// Validate terminal - if configured terminal doesn't exist, auto-detect
|
||||
if !command_exists(&config.general.terminal_command) {
|
||||
warn!(
|
||||
"Configured terminal '{}' not found, auto-detecting",
|
||||
config.general.terminal_command
|
||||
);
|
||||
config.general.terminal_command = detect_terminal();
|
||||
info!("Using detected terminal: {}", config.general.terminal_command);
|
||||
// Auto-detect terminal if not configured or configured terminal doesn't exist
|
||||
match &config.general.terminal_command {
|
||||
None => {
|
||||
let terminal = detect_terminal();
|
||||
info!("Detected terminal: {}", terminal);
|
||||
config.general.terminal_command = Some(terminal);
|
||||
}
|
||||
Some(term) if !command_exists(term) => {
|
||||
warn!("Configured terminal '{}' not found, auto-detecting", term);
|
||||
let terminal = detect_terminal();
|
||||
info!("Using detected terminal: {}", terminal);
|
||||
config.general.terminal_command = Some(terminal);
|
||||
}
|
||||
Some(term) => {
|
||||
debug!("Using configured terminal: {}", term);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
|
||||
@@ -37,35 +37,45 @@ impl ProviderFilter {
|
||||
} else {
|
||||
// Use config file settings, default to apps only
|
||||
let mut set = HashSet::new();
|
||||
// Core providers
|
||||
if config_providers.applications {
|
||||
set.insert(ProviderType::Application);
|
||||
}
|
||||
if config_providers.commands {
|
||||
set.insert(ProviderType::Command);
|
||||
}
|
||||
// Plugin providers - use Plugin(type_id) for all
|
||||
if config_providers.uuctl {
|
||||
set.insert(ProviderType::Uuctl);
|
||||
set.insert(ProviderType::Plugin("uuctl".to_string()));
|
||||
}
|
||||
if config_providers.system {
|
||||
set.insert(ProviderType::System);
|
||||
set.insert(ProviderType::Plugin("system".to_string()));
|
||||
}
|
||||
if config_providers.ssh {
|
||||
set.insert(ProviderType::Ssh);
|
||||
set.insert(ProviderType::Plugin("ssh".to_string()));
|
||||
}
|
||||
if config_providers.clipboard {
|
||||
set.insert(ProviderType::Clipboard);
|
||||
set.insert(ProviderType::Plugin("clipboard".to_string()));
|
||||
}
|
||||
if config_providers.bookmarks {
|
||||
set.insert(ProviderType::Bookmarks);
|
||||
set.insert(ProviderType::Plugin("bookmarks".to_string()));
|
||||
}
|
||||
if config_providers.emoji {
|
||||
set.insert(ProviderType::Emoji);
|
||||
set.insert(ProviderType::Plugin("emoji".to_string()));
|
||||
}
|
||||
if config_providers.scripts {
|
||||
set.insert(ProviderType::Scripts);
|
||||
set.insert(ProviderType::Plugin("scripts".to_string()));
|
||||
}
|
||||
// Dynamic providers
|
||||
if config_providers.files {
|
||||
set.insert(ProviderType::Plugin("filesearch".to_string()));
|
||||
}
|
||||
if config_providers.calculator {
|
||||
set.insert(ProviderType::Plugin("calc".to_string()));
|
||||
}
|
||||
if config_providers.websearch {
|
||||
set.insert(ProviderType::Plugin("websearch".to_string()));
|
||||
}
|
||||
// Note: Files, Calculator, WebSearch are dynamic providers
|
||||
// that don't need to be in the filter set - they're triggered by prefix
|
||||
// Default to apps if nothing enabled
|
||||
if set.is_empty() {
|
||||
set.insert(ProviderType::Application);
|
||||
@@ -104,9 +114,11 @@ impl ProviderFilter {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||
} else {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
let provider_debug = format!("{:?}", provider);
|
||||
self.enabled.insert(provider);
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled ON {:?}, enabled: {:?}", provider, self.enabled);
|
||||
debug!("[Filter] Toggled ON {}, enabled: {:?}", provider_debug, self.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +171,7 @@ impl ProviderFilter {
|
||||
}
|
||||
|
||||
/// Parse query for prefix syntax
|
||||
/// Prefixes map to Plugin(type_id) for plugin providers
|
||||
pub fn parse_query(query: &str) -> ParsedQuery {
|
||||
let trimmed = query.trim_start();
|
||||
|
||||
@@ -186,37 +199,57 @@ impl ProviderFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// Check for prefix patterns (with trailing space)
|
||||
let prefixes = [
|
||||
// Core provider prefixes
|
||||
let core_prefixes: &[(&str, ProviderType)] = &[
|
||||
(":app ", ProviderType::Application),
|
||||
(":apps ", ProviderType::Application),
|
||||
(":bm ", ProviderType::Bookmarks),
|
||||
(":bookmark ", ProviderType::Bookmarks),
|
||||
(":bookmarks ", ProviderType::Bookmarks),
|
||||
(":calc ", ProviderType::Calculator),
|
||||
(":calculator ", ProviderType::Calculator),
|
||||
(":clip ", ProviderType::Clipboard),
|
||||
(":clipboard ", ProviderType::Clipboard),
|
||||
(":cmd ", ProviderType::Command),
|
||||
(":command ", ProviderType::Command),
|
||||
(":emoji ", ProviderType::Emoji),
|
||||
(":emojis ", ProviderType::Emoji),
|
||||
(":file ", ProviderType::Files),
|
||||
(":files ", ProviderType::Files),
|
||||
(":find ", ProviderType::Files),
|
||||
(":script ", ProviderType::Scripts),
|
||||
(":scripts ", ProviderType::Scripts),
|
||||
(":ssh ", ProviderType::Ssh),
|
||||
(":sys ", ProviderType::System),
|
||||
(":system ", ProviderType::System),
|
||||
(":power ", ProviderType::System),
|
||||
(":uuctl ", ProviderType::Uuctl),
|
||||
(":web ", ProviderType::WebSearch),
|
||||
(":search ", ProviderType::WebSearch),
|
||||
];
|
||||
|
||||
for (prefix_str, provider) in prefixes {
|
||||
// Plugin provider prefixes - mapped to Plugin(type_id)
|
||||
let plugin_prefixes: &[(&str, &str)] = &[
|
||||
(":bm ", "bookmarks"),
|
||||
(":bookmark ", "bookmarks"),
|
||||
(":bookmarks ", "bookmarks"),
|
||||
(":calc ", "calc"),
|
||||
(":calculator ", "calc"),
|
||||
(":clip ", "clipboard"),
|
||||
(":clipboard ", "clipboard"),
|
||||
(":emoji ", "emoji"),
|
||||
(":emojis ", "emoji"),
|
||||
(":file ", "filesearch"),
|
||||
(":files ", "filesearch"),
|
||||
(":find ", "filesearch"),
|
||||
(":script ", "scripts"),
|
||||
(":scripts ", "scripts"),
|
||||
(":ssh ", "ssh"),
|
||||
(":sys ", "system"),
|
||||
(":system ", "system"),
|
||||
(":power ", "system"),
|
||||
(":uuctl ", "uuctl"),
|
||||
(":systemd ", "uuctl"),
|
||||
(":web ", "websearch"),
|
||||
(":search ", "websearch"),
|
||||
];
|
||||
|
||||
// Check core prefixes
|
||||
for (prefix_str, provider) in core_prefixes {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider.clone()),
|
||||
tag_filter: None,
|
||||
query: rest.to_string(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check plugin prefixes
|
||||
for (prefix_str, type_id) in plugin_prefixes {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||
let provider = ProviderType::Plugin(type_id.to_string());
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||
return ParsedQuery {
|
||||
@@ -227,37 +260,54 @@ impl ProviderFilter {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle prefix without trailing space (still typing)
|
||||
let partial_prefixes = [
|
||||
// Handle partial prefixes (still typing)
|
||||
let partial_core: &[(&str, ProviderType)] = &[
|
||||
(":app", ProviderType::Application),
|
||||
(":apps", ProviderType::Application),
|
||||
(":bm", ProviderType::Bookmarks),
|
||||
(":bookmark", ProviderType::Bookmarks),
|
||||
(":bookmarks", ProviderType::Bookmarks),
|
||||
(":calc", ProviderType::Calculator),
|
||||
(":calculator", ProviderType::Calculator),
|
||||
(":clip", ProviderType::Clipboard),
|
||||
(":clipboard", ProviderType::Clipboard),
|
||||
(":cmd", ProviderType::Command),
|
||||
(":command", ProviderType::Command),
|
||||
(":emoji", ProviderType::Emoji),
|
||||
(":emojis", ProviderType::Emoji),
|
||||
(":file", ProviderType::Files),
|
||||
(":files", ProviderType::Files),
|
||||
(":find", ProviderType::Files),
|
||||
(":script", ProviderType::Scripts),
|
||||
(":scripts", ProviderType::Scripts),
|
||||
(":ssh", ProviderType::Ssh),
|
||||
(":sys", ProviderType::System),
|
||||
(":system", ProviderType::System),
|
||||
(":power", ProviderType::System),
|
||||
(":uuctl", ProviderType::Uuctl),
|
||||
(":web", ProviderType::WebSearch),
|
||||
(":search", ProviderType::WebSearch),
|
||||
];
|
||||
|
||||
for (prefix_str, provider) in partial_prefixes {
|
||||
if trimmed == prefix_str {
|
||||
let partial_plugin: &[(&str, &str)] = &[
|
||||
(":bm", "bookmarks"),
|
||||
(":bookmark", "bookmarks"),
|
||||
(":bookmarks", "bookmarks"),
|
||||
(":calc", "calc"),
|
||||
(":calculator", "calc"),
|
||||
(":clip", "clipboard"),
|
||||
(":clipboard", "clipboard"),
|
||||
(":emoji", "emoji"),
|
||||
(":emojis", "emoji"),
|
||||
(":file", "filesearch"),
|
||||
(":files", "filesearch"),
|
||||
(":find", "filesearch"),
|
||||
(":script", "scripts"),
|
||||
(":scripts", "scripts"),
|
||||
(":ssh", "ssh"),
|
||||
(":sys", "system"),
|
||||
(":system", "system"),
|
||||
(":power", "system"),
|
||||
(":uuctl", "uuctl"),
|
||||
(":systemd", "uuctl"),
|
||||
(":web", "websearch"),
|
||||
(":search", "websearch"),
|
||||
];
|
||||
|
||||
for (prefix_str, provider) in partial_core {
|
||||
if trimmed == *prefix_str {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider.clone()),
|
||||
tag_filter: None,
|
||||
query: String::new(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
for (prefix_str, type_id) in partial_plugin {
|
||||
if trimmed == *prefix_str {
|
||||
let provider = ProviderType::Plugin(type_id.to_string());
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||
return ParsedQuery {
|
||||
@@ -285,22 +335,9 @@ impl ProviderFilter {
|
||||
let mut providers: Vec<_> = self.enabled.iter().cloned().collect();
|
||||
providers.sort_by_key(|p| match p {
|
||||
ProviderType::Application => 0,
|
||||
ProviderType::Bookmarks => 1,
|
||||
ProviderType::Calculator => 2,
|
||||
ProviderType::Clipboard => 3,
|
||||
ProviderType::Command => 4,
|
||||
ProviderType::Dmenu => 5,
|
||||
ProviderType::Emoji => 6,
|
||||
ProviderType::Files => 7,
|
||||
ProviderType::MediaPlayer => 8,
|
||||
ProviderType::Pomodoro => 9,
|
||||
ProviderType::Scripts => 10,
|
||||
ProviderType::Ssh => 11,
|
||||
ProviderType::System => 12,
|
||||
ProviderType::Uuctl => 13,
|
||||
ProviderType::Weather => 14,
|
||||
ProviderType::WebSearch => 15,
|
||||
ProviderType::Plugin(_) => 100, // Plugin providers sort last
|
||||
ProviderType::Command => 1,
|
||||
ProviderType::Dmenu => 2,
|
||||
ProviderType::Plugin(_) => 100, // Plugin providers sort after core
|
||||
});
|
||||
providers
|
||||
}
|
||||
@@ -310,21 +347,8 @@ impl ProviderFilter {
|
||||
if let Some(ref prefix) = self.active_prefix {
|
||||
return match prefix {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Bookmarks => "Bookmarks",
|
||||
ProviderType::Calculator => "Calc",
|
||||
ProviderType::Clipboard => "Clipboard",
|
||||
ProviderType::Command => "Commands",
|
||||
ProviderType::Dmenu => "dmenu",
|
||||
ProviderType::Emoji => "Emoji",
|
||||
ProviderType::Files => "Files",
|
||||
ProviderType::MediaPlayer => "Media",
|
||||
ProviderType::Pomodoro => "Pomodoro",
|
||||
ProviderType::Scripts => "Scripts",
|
||||
ProviderType::Ssh => "SSH",
|
||||
ProviderType::System => "System",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::Weather => "Weather",
|
||||
ProviderType::WebSearch => "Web",
|
||||
ProviderType::Plugin(_) => "Plugin",
|
||||
};
|
||||
}
|
||||
@@ -333,21 +357,8 @@ impl ProviderFilter {
|
||||
if enabled.len() == 1 {
|
||||
match &enabled[0] {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Bookmarks => "Bookmarks",
|
||||
ProviderType::Calculator => "Calc",
|
||||
ProviderType::Clipboard => "Clipboard",
|
||||
ProviderType::Command => "Commands",
|
||||
ProviderType::Dmenu => "dmenu",
|
||||
ProviderType::Emoji => "Emoji",
|
||||
ProviderType::Files => "Files",
|
||||
ProviderType::MediaPlayer => "Media",
|
||||
ProviderType::Pomodoro => "Pomodoro",
|
||||
ProviderType::Scripts => "Scripts",
|
||||
ProviderType::Ssh => "SSH",
|
||||
ProviderType::System => "System",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::Weather => "Weather",
|
||||
ProviderType::WebSearch => "Web",
|
||||
ProviderType::Plugin(_) => "Plugin",
|
||||
}
|
||||
} else {
|
||||
@@ -381,6 +392,13 @@ mod tests {
|
||||
assert_eq!(result.query, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_query_plugin_prefix() {
|
||||
let result = ProviderFilter::parse_query(":calc 5+3");
|
||||
assert_eq!(result.prefix, Some(ProviderType::Plugin("calc".to_string())));
|
||||
assert_eq!(result.query, "5+3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toggle_ensures_one_enabled() {
|
||||
let mut filter = ProviderFilter::apps_only();
|
||||
|
||||
@@ -99,23 +99,57 @@ pub fn frecency_file() -> Option<PathBuf> {
|
||||
// =============================================================================
|
||||
|
||||
/// System data directories for applications (XDG_DATA_DIRS)
|
||||
///
|
||||
/// Follows the XDG Base Directory Specification:
|
||||
/// - $XDG_DATA_HOME/applications (defaults to ~/.local/share/applications)
|
||||
/// - $XDG_DATA_DIRS/*/applications (defaults to /usr/local/share:/usr/share)
|
||||
/// - Additional Flatpak and Snap directories
|
||||
pub fn system_data_dirs() -> Vec<PathBuf> {
|
||||
let mut dirs = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
|
||||
// User data directory first
|
||||
// Helper to add unique directories
|
||||
let mut add_dir = |path: PathBuf| {
|
||||
if seen.insert(path.clone()) {
|
||||
dirs.push(path);
|
||||
}
|
||||
};
|
||||
|
||||
// 1. User data directory first (highest priority)
|
||||
if let Some(data) = data_home() {
|
||||
dirs.push(data.join("applications"));
|
||||
add_dir(data.join("applications"));
|
||||
}
|
||||
|
||||
// System directories
|
||||
dirs.push(PathBuf::from("/usr/share/applications"));
|
||||
dirs.push(PathBuf::from("/usr/local/share/applications"));
|
||||
// 2. XDG_DATA_DIRS - parse the environment variable
|
||||
// Default per spec: /usr/local/share:/usr/share
|
||||
let xdg_data_dirs = std::env::var("XDG_DATA_DIRS")
|
||||
.unwrap_or_else(|_| "/usr/local/share:/usr/share".to_string());
|
||||
|
||||
// Flatpak directories
|
||||
if let Some(data) = data_home() {
|
||||
dirs.push(data.join("flatpak/exports/share/applications"));
|
||||
for dir in xdg_data_dirs.split(':') {
|
||||
if !dir.is_empty() {
|
||||
add_dir(PathBuf::from(dir).join("applications"));
|
||||
}
|
||||
}
|
||||
dirs.push(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
||||
|
||||
// 3. Always include standard system directories as fallback
|
||||
// Some environments set XDG_DATA_DIRS without including these
|
||||
add_dir(PathBuf::from("/usr/share/applications"));
|
||||
add_dir(PathBuf::from("/usr/local/share/applications"));
|
||||
|
||||
// 4. Flatpak directories (user and system)
|
||||
if let Some(data) = data_home() {
|
||||
add_dir(data.join("flatpak/exports/share/applications"));
|
||||
}
|
||||
add_dir(PathBuf::from("/var/lib/flatpak/exports/share/applications"));
|
||||
|
||||
// 5. Snap directories
|
||||
add_dir(PathBuf::from("/var/lib/snapd/desktop/applications"));
|
||||
|
||||
// 6. Nix directories (common on NixOS)
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
add_dir(home.join(".nix-profile/share/applications"));
|
||||
}
|
||||
add_dir(PathBuf::from("/run/current-system/sw/share/applications"));
|
||||
|
||||
dirs
|
||||
}
|
||||
|
||||
@@ -156,6 +156,7 @@ pub fn discover_plugins(plugins_dir: &Path) -> PluginResult<HashMap<String, (Plu
|
||||
}
|
||||
|
||||
/// Check if a plugin is compatible with the given owlry version
|
||||
#[allow(dead_code)]
|
||||
pub fn check_compatibility(manifest: &PluginManifest, owlry_version: &str) -> PluginResult<()> {
|
||||
if !manifest.is_compatible_with(owlry_version) {
|
||||
return Err(PluginError::VersionMismatch {
|
||||
@@ -230,6 +231,7 @@ impl PluginManifest {
|
||||
}
|
||||
|
||||
/// Check if this plugin is compatible with the given owlry version
|
||||
#[allow(dead_code)]
|
||||
pub fn is_compatible_with(&self, owlry_version: &str) -> bool {
|
||||
let req = match semver::VersionReq::parse(&self.plugin.owlry_version) {
|
||||
Ok(r) => r,
|
||||
|
||||
@@ -43,6 +43,7 @@ pub use api::provider::{PluginItem, ProviderRegistration};
|
||||
#[allow(unused_imports)]
|
||||
pub use api::{ActionRegistration, HookEvent, ThemeRegistration};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use error::{PluginError, PluginResult};
|
||||
|
||||
#[cfg(feature = "lua")]
|
||||
|
||||
@@ -271,14 +271,16 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_lua_runtime_not_installed() {
|
||||
// In test environment, runtime shouldn't be installed
|
||||
assert!(!lua_runtime_available());
|
||||
fn test_lua_runtime_check_doesnt_panic() {
|
||||
// Just verify the function runs without panicking
|
||||
// Result depends on whether runtime is installed
|
||||
let _available = lua_runtime_available();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rune_runtime_not_installed() {
|
||||
// In test environment, runtime shouldn't be installed
|
||||
assert!(!rune_runtime_available());
|
||||
fn test_rune_runtime_check_doesnt_panic() {
|
||||
// Just verify the function runs without panicking
|
||||
// Result depends on whether runtime is installed
|
||||
let _available = rune_runtime_available();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,15 @@ impl Provider for ApplicationProvider {
|
||||
// Empty locale list for default locale
|
||||
let locales: &[&str] = &[];
|
||||
|
||||
// Get current desktop environment(s) for OnlyShowIn/NotShowIn filtering
|
||||
// XDG_CURRENT_DESKTOP can be colon-separated (e.g., "ubuntu:GNOME")
|
||||
let current_desktops: Vec<String> = std::env::var("XDG_CURRENT_DESKTOP")
|
||||
.unwrap_or_default()
|
||||
.split(':')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.to_string())
|
||||
.collect();
|
||||
|
||||
for path in Iter::new(dirs.into_iter()) {
|
||||
let content = match std::fs::read_to_string(&path) {
|
||||
Ok(c) => c,
|
||||
@@ -125,6 +134,24 @@ impl Provider for ApplicationProvider {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply OnlyShowIn/NotShowIn filters only if we know the current desktop
|
||||
// If XDG_CURRENT_DESKTOP is not set, show all apps (don't filter)
|
||||
if !current_desktops.is_empty() {
|
||||
// OnlyShowIn: if set, current desktop must be in the list
|
||||
if desktop_entry.only_show_in().is_some_and(|only| {
|
||||
!current_desktops.iter().any(|de| only.contains(&de.as_str()))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// NotShowIn: if current desktop is in the list, skip
|
||||
if desktop_entry.not_show_in().is_some_and(|not| {
|
||||
current_desktops.iter().any(|de| not.contains(&de.as_str()))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let name = match desktop_entry.name(locales) {
|
||||
Some(n) => n.to_string(),
|
||||
None => continue,
|
||||
@@ -135,12 +162,17 @@ impl Provider for ApplicationProvider {
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Extract categories as tags (lowercase for consistency)
|
||||
let tags: Vec<String> = desktop_entry
|
||||
// Extract categories and keywords as tags (lowercase for consistency)
|
||||
let mut tags: Vec<String> = desktop_entry
|
||||
.categories()
|
||||
.map(|cats| cats.into_iter().map(|s| s.to_lowercase()).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
// Add keywords for searchability (e.g., Nautilus has Name=Files but Keywords contains "nautilus")
|
||||
if let Some(keywords) = desktop_entry.keywords(locales) {
|
||||
tags.extend(keywords.into_iter().map(|s| s.to_lowercase()));
|
||||
}
|
||||
|
||||
let item = LaunchItem {
|
||||
id: path.to_string_lossy().to_string(),
|
||||
name,
|
||||
@@ -157,6 +189,13 @@ impl Provider for ApplicationProvider {
|
||||
|
||||
debug!("Found {} applications", self.items.len());
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!(
|
||||
"XDG_CURRENT_DESKTOP={:?}, scanned dirs count={}",
|
||||
current_desktops,
|
||||
Self::get_application_dirs().len()
|
||||
);
|
||||
|
||||
// Sort alphabetically by name
|
||||
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
}
|
||||
@@ -210,4 +249,18 @@ mod tests {
|
||||
"bash -c 'echo %u'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clean_desktop_exec_preserves_env() {
|
||||
// env VAR=value pattern should be preserved
|
||||
assert_eq!(
|
||||
clean_desktop_exec_field("env GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 audacity %F"),
|
||||
"env GDK_BACKEND=x11 UBUNTU_MENUPROXY=0 audacity"
|
||||
);
|
||||
// Multiple env vars
|
||||
assert_eq!(
|
||||
clean_desktop_exec_field("env FOO=bar BAZ=qux myapp %u"),
|
||||
"env FOO=bar BAZ=qux myapp"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,27 +44,17 @@ pub struct LaunchItem {
|
||||
|
||||
/// Provider type identifier for filtering and badge display
|
||||
///
|
||||
/// Note: Plugin is a special case that stores a type_id string
|
||||
/// for custom plugin-defined provider types.
|
||||
/// Core types are built-in providers. All native plugins use Plugin(type_id).
|
||||
/// This keeps the core app free of plugin-specific knowledge.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum ProviderType {
|
||||
/// Built-in: Desktop applications from XDG directories
|
||||
Application,
|
||||
Bookmarks,
|
||||
Calculator,
|
||||
Clipboard,
|
||||
/// Built-in: Shell commands from PATH
|
||||
Command,
|
||||
/// Built-in: Pipe-based input (dmenu compatibility)
|
||||
Dmenu,
|
||||
Emoji,
|
||||
Files,
|
||||
MediaPlayer,
|
||||
Pomodoro,
|
||||
Scripts,
|
||||
Ssh,
|
||||
System,
|
||||
Uuctl,
|
||||
Weather,
|
||||
WebSearch,
|
||||
/// Plugin-defined provider type with custom type_id
|
||||
/// Plugin-defined provider type with its type_id (e.g., "calc", "weather", "emoji")
|
||||
Plugin(String),
|
||||
}
|
||||
|
||||
@@ -73,27 +63,11 @@ impl std::str::FromStr for ProviderType {
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.to_lowercase().as_str() {
|
||||
// Core built-in providers
|
||||
"app" | "apps" | "application" | "applications" => Ok(ProviderType::Application),
|
||||
"bookmark" | "bookmarks" | "bm" => Ok(ProviderType::Bookmarks),
|
||||
"calc" | "calculator" => Ok(ProviderType::Calculator),
|
||||
"clip" | "clipboard" => Ok(ProviderType::Clipboard),
|
||||
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
|
||||
"dmenu" => Ok(ProviderType::Dmenu),
|
||||
"emoji" | "emojis" => Ok(ProviderType::Emoji),
|
||||
"file" | "files" | "find" => Ok(ProviderType::Files),
|
||||
"media" | "mpris" | "player" => Ok(ProviderType::MediaPlayer),
|
||||
"pomo" | "pomodoro" | "timer" => Ok(ProviderType::Pomodoro),
|
||||
"script" | "scripts" => Ok(ProviderType::Scripts),
|
||||
"ssh" => Ok(ProviderType::Ssh),
|
||||
"sys" | "system" | "power" => Ok(ProviderType::System),
|
||||
"uuctl" => Ok(ProviderType::Uuctl),
|
||||
"weather" => Ok(ProviderType::Weather),
|
||||
"web" | "websearch" | "search" => Ok(ProviderType::WebSearch),
|
||||
// Plugin types are prefixed with "plugin:" (e.g., "plugin:github-repos")
|
||||
other if other.starts_with("plugin:") => {
|
||||
Ok(ProviderType::Plugin(other[7..].to_string()))
|
||||
}
|
||||
// Unknown types become plugin types
|
||||
// Everything else is a plugin
|
||||
other => Ok(ProviderType::Plugin(other.to_string())),
|
||||
}
|
||||
}
|
||||
@@ -103,21 +77,8 @@ impl std::fmt::Display for ProviderType {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ProviderType::Application => write!(f, "app"),
|
||||
ProviderType::Bookmarks => write!(f, "bookmark"),
|
||||
ProviderType::Calculator => write!(f, "calc"),
|
||||
ProviderType::Clipboard => write!(f, "clip"),
|
||||
ProviderType::Command => write!(f, "cmd"),
|
||||
ProviderType::Dmenu => write!(f, "dmenu"),
|
||||
ProviderType::Emoji => write!(f, "emoji"),
|
||||
ProviderType::Files => write!(f, "file"),
|
||||
ProviderType::MediaPlayer => write!(f, "media"),
|
||||
ProviderType::Pomodoro => write!(f, "pomo"),
|
||||
ProviderType::Scripts => write!(f, "script"),
|
||||
ProviderType::Ssh => write!(f, "ssh"),
|
||||
ProviderType::System => write!(f, "sys"),
|
||||
ProviderType::Uuctl => write!(f, "uuctl"),
|
||||
ProviderType::Weather => write!(f, "weather"),
|
||||
ProviderType::WebSearch => write!(f, "web"),
|
||||
ProviderType::Plugin(type_id) => write!(f, "{}", type_id),
|
||||
}
|
||||
}
|
||||
@@ -134,8 +95,10 @@ pub trait Provider: Send {
|
||||
|
||||
/// Manages all providers and handles searching
|
||||
pub struct ProviderManager {
|
||||
/// Static providers (apps, commands, and native static plugins)
|
||||
/// Core static providers (apps, commands, dmenu)
|
||||
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)
|
||||
/// These are queried per-keystroke, not cached
|
||||
dynamic_providers: Vec<NativeProvider>,
|
||||
@@ -146,22 +109,18 @@ pub struct ProviderManager {
|
||||
matcher: SkimMatcherV2,
|
||||
}
|
||||
|
||||
/// Known dynamic provider type IDs (need per-query evaluation)
|
||||
const DYNAMIC_TYPE_IDS: &[&str] = &["calc", "websearch", "filesearch"];
|
||||
|
||||
/// Known widget provider type IDs (appear at top of results)
|
||||
const WIDGET_TYPE_IDS: &[&str] = &["weather", "media", "pomodoro"];
|
||||
|
||||
impl ProviderManager {
|
||||
/// Create a new ProviderManager with native plugins
|
||||
///
|
||||
/// Native plugins are loaded from /usr/lib/owlry/plugins/ and categorized into:
|
||||
/// - Static providers (added to providers vec)
|
||||
/// - Dynamic providers (queried per-keystroke: calculator, websearch, filesearch)
|
||||
/// - Widget providers (shown at top: weather, media, pomodoro)
|
||||
/// Native plugins are loaded from /usr/lib/owlry/plugins/ and categorized based on
|
||||
/// their declared ProviderKind and ProviderPosition:
|
||||
/// - Static providers with Normal position (added to providers vec)
|
||||
/// - Dynamic providers (queried per-keystroke, declared via ProviderKind::Dynamic)
|
||||
/// - Widget providers (shown at top, declared via ProviderPosition::Widget)
|
||||
pub fn with_native_plugins(native_providers: Vec<NativeProvider>) -> Self {
|
||||
let mut manager = Self {
|
||||
providers: Vec::new(),
|
||||
static_native_providers: Vec::new(),
|
||||
dynamic_providers: Vec::new(),
|
||||
widget_providers: Vec::new(),
|
||||
matcher: SkimMatcherV2::default(),
|
||||
@@ -180,19 +139,22 @@ impl ProviderManager {
|
||||
manager.providers.push(Box::new(ApplicationProvider::new()));
|
||||
manager.providers.push(Box::new(CommandProvider::new()));
|
||||
|
||||
// Categorize native plugins
|
||||
// Categorize native plugins based on their declared ProviderKind and ProviderPosition
|
||||
for provider in native_providers {
|
||||
let type_id = provider.type_id();
|
||||
|
||||
if DYNAMIC_TYPE_IDS.contains(&type_id) {
|
||||
if provider.is_dynamic() {
|
||||
// Dynamic providers declare ProviderKind::Dynamic
|
||||
info!("Registered dynamic provider: {} ({})", provider.name(), type_id);
|
||||
manager.dynamic_providers.push(provider);
|
||||
} else if WIDGET_TYPE_IDS.contains(&type_id) {
|
||||
} else if provider.is_widget() {
|
||||
// Widgets declare ProviderPosition::Widget
|
||||
info!("Registered widget provider: {} ({})", provider.name(), type_id);
|
||||
manager.widget_providers.push(provider);
|
||||
} else {
|
||||
// Static native providers (keep as NativeProvider for query/submenu support)
|
||||
info!("Registered static provider: {} ({})", provider.name(), type_id);
|
||||
manager.providers.push(Box::new(provider));
|
||||
manager.static_native_providers.push(provider);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -211,7 +173,7 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
pub fn refresh_all(&mut self) {
|
||||
// Refresh static providers (fast, local operations)
|
||||
// Refresh core providers (apps, commands)
|
||||
for provider in &mut self.providers {
|
||||
provider.refresh();
|
||||
info!(
|
||||
@@ -221,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
|
||||
// Call refresh_widgets() after window is shown
|
||||
|
||||
@@ -242,9 +214,13 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
/// 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> {
|
||||
// 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) {
|
||||
return Some(p);
|
||||
}
|
||||
@@ -273,49 +249,54 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
/// Add a dynamic provider (e.g., from a Lua plugin)
|
||||
#[allow(dead_code)]
|
||||
pub fn add_provider(&mut self, provider: Box<dyn Provider>) {
|
||||
info!("Added plugin provider: {}", provider.name());
|
||||
self.providers.push(provider);
|
||||
}
|
||||
|
||||
/// Add multiple providers at once (for batch plugin loading)
|
||||
#[allow(dead_code)]
|
||||
pub fn add_providers(&mut self, providers: Vec<Box<dyn Provider>>) {
|
||||
for provider in providers {
|
||||
self.add_provider(provider);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
|
||||
if query.is_empty() {
|
||||
// Return recent/popular items when query is empty
|
||||
return self.providers
|
||||
.iter()
|
||||
.flat_map(|p| p.items().iter().cloned())
|
||||
return self.all_static_items()
|
||||
.take(max_results)
|
||||
.map(|item| (item, 0))
|
||||
.map(|item| (item.clone(), 0))
|
||||
.collect();
|
||||
}
|
||||
|
||||
let mut results: Vec<(LaunchItem, i64)> = self.providers
|
||||
.iter()
|
||||
.flat_map(|provider| {
|
||||
provider.items().iter().filter_map(|item| {
|
||||
// Match against name and description
|
||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||
let desc_score = item.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
let mut results: Vec<(LaunchItem, i64)> = self.all_static_items()
|
||||
.filter_map(|item| {
|
||||
// Match against name and description
|
||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||
let desc_score = item.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
|
||||
let score = match (name_score, desc_score) {
|
||||
(Some(n), Some(d)) => Some(n.max(d)),
|
||||
(Some(n), None) => Some(n),
|
||||
(None, Some(d)) => Some(d / 2), // Lower weight for description matches
|
||||
(None, None) => None,
|
||||
};
|
||||
let score = match (name_score, desc_score) {
|
||||
(Some(n), Some(d)) => Some(n.max(d)),
|
||||
(Some(n), None) => Some(n),
|
||||
(None, Some(d)) => Some(d / 2), // Lower weight for description matches
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
score.map(|s| (item.clone(), s))
|
||||
})
|
||||
score.map(|s| (item.clone(), s))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -332,38 +313,45 @@ impl ProviderManager {
|
||||
max_results: usize,
|
||||
filter: &crate::filter::ProviderFilter,
|
||||
) -> 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() {
|
||||
return self
|
||||
.providers
|
||||
.iter()
|
||||
.filter(|p| filter.is_active(p.provider_type()))
|
||||
.flat_map(|p| p.items().iter().cloned())
|
||||
return core_items
|
||||
.chain(native_items)
|
||||
.take(max_results)
|
||||
.map(|item| (item, 0))
|
||||
.collect();
|
||||
}
|
||||
|
||||
let mut results: Vec<(LaunchItem, i64)> = self
|
||||
.providers
|
||||
.iter()
|
||||
.filter(|provider| filter.is_active(provider.provider_type()))
|
||||
.flat_map(|provider| {
|
||||
provider.items().iter().filter_map(|item| {
|
||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||
let desc_score = item
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
let mut results: Vec<(LaunchItem, i64)> = core_items
|
||||
.chain(native_items)
|
||||
.filter_map(|item| {
|
||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||
let desc_score = item
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
|
||||
let score = match (name_score, desc_score) {
|
||||
(Some(n), Some(d)) => Some(n.max(d)),
|
||||
(Some(n), None) => Some(n),
|
||||
(None, Some(d)) => Some(d / 2),
|
||||
(None, None) => None,
|
||||
};
|
||||
let score = match (name_score, desc_score) {
|
||||
(Some(n), Some(d)) => Some(n.max(d)),
|
||||
(Some(n), None) => Some(n),
|
||||
(None, Some(d)) => Some(d / 2),
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
score.map(|s| (item.clone(), s))
|
||||
})
|
||||
score.map(|s| (item, s))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -391,15 +379,11 @@ impl ProviderManager {
|
||||
// 1. No specific filter prefix is active
|
||||
// 2. Query is empty (user hasn't started searching)
|
||||
// This keeps widgets visible on launch but hides them during active search
|
||||
// Widgets are always visible regardless of filter settings (they declare position via API)
|
||||
if filter.active_prefix().is_none() && query.is_empty() {
|
||||
// Widget priority scores based on type
|
||||
// Widget priority comes from plugin-declared priority field
|
||||
for provider in &self.widget_providers {
|
||||
let base_score = match provider.type_id() {
|
||||
"weather" => 12000,
|
||||
"pomodoro" => 11500,
|
||||
"media" => 11000,
|
||||
_ => 10500,
|
||||
};
|
||||
let base_score = provider.priority() as i64;
|
||||
for (idx, item) in provider.items().iter().enumerate() {
|
||||
results.push((item.clone(), base_score - idx as i64));
|
||||
}
|
||||
@@ -407,16 +391,18 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
// Query dynamic providers (calculator, websearch, filesearch)
|
||||
// Each provider internally checks if the query matches its prefix
|
||||
// Only query if:
|
||||
// 1. Their specific filter is active (e.g., :file prefix or Files tab selected), OR
|
||||
// 2. No specific single-mode filter is active (showing all providers)
|
||||
if !query.is_empty() {
|
||||
for (provider_idx, provider) in self.dynamic_providers.iter().enumerate() {
|
||||
for provider in &self.dynamic_providers {
|
||||
// Skip if this provider type is explicitly filtered out
|
||||
if !filter.is_active(provider.provider_type()) {
|
||||
continue;
|
||||
}
|
||||
let dynamic_results = provider.query(query);
|
||||
let base_score = match provider.type_id() {
|
||||
"calc" => 10000,
|
||||
"websearch" => 9000,
|
||||
"filesearch" => 8000,
|
||||
_ => 7000 - (provider_idx as i64 * 1000),
|
||||
};
|
||||
// Priority comes from plugin-declared priority field
|
||||
let base_score = provider.priority() as i64;
|
||||
for (idx, item) in dynamic_results.into_iter().enumerate() {
|
||||
results.push((item, base_score - idx as i64));
|
||||
}
|
||||
@@ -425,11 +411,22 @@ impl ProviderManager {
|
||||
|
||||
// Empty query (after checking special providers) - return frecency-sorted items
|
||||
if query.is_empty() {
|
||||
let items: Vec<(LaunchItem, i64)> = self
|
||||
// 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())
|
||||
.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| {
|
||||
// Apply tag filter if present
|
||||
if let Some(tag) = tag_filter {
|
||||
@@ -453,53 +450,70 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
// Regular search with frecency boost and tag matching
|
||||
let search_results: Vec<(LaunchItem, i64)> = self
|
||||
.providers
|
||||
.iter()
|
||||
.filter(|provider| filter.is_active(provider.provider_type()))
|
||||
.flat_map(|provider| {
|
||||
provider.items().iter().filter_map(|item| {
|
||||
// Apply tag filter if present
|
||||
if let Some(tag) = tag_filter
|
||||
&& !item.tags.iter().any(|t| t.to_lowercase().contains(tag)) {
|
||||
return None;
|
||||
}
|
||||
// Helper closure for scoring items
|
||||
let score_item = |item: &LaunchItem| -> Option<(LaunchItem, i64)> {
|
||||
// Apply tag filter if present
|
||||
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 desc_score = item
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||
let desc_score = item
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
|
||||
// Also match against tags (lower weight)
|
||||
let tag_score = item
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| self.matcher.fuzzy_match(t, query))
|
||||
.max()
|
||||
.map(|s| s / 3); // Lower weight for tag matches
|
||||
// Also match against tags (lower weight)
|
||||
let tag_score = item
|
||||
.tags
|
||||
.iter()
|
||||
.filter_map(|t| self.matcher.fuzzy_match(t, query))
|
||||
.max()
|
||||
.map(|s| s / 3); // Lower weight for tag matches
|
||||
|
||||
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), None) => Some(n.max(d)),
|
||||
(Some(n), None, Some(t)) => Some(n.max(t)),
|
||||
(Some(n), None, None) => Some(n),
|
||||
(None, Some(d), Some(t)) => Some((d / 2).max(t)),
|
||||
(None, Some(d), None) => Some(d / 2),
|
||||
(None, None, Some(t)) => Some(t),
|
||||
(None, None, None) => None,
|
||||
};
|
||||
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), None) => Some(n.max(d)),
|
||||
(Some(n), None, Some(t)) => Some(n.max(t)),
|
||||
(Some(n), None, None) => Some(n),
|
||||
(None, Some(d), Some(t)) => Some((d / 2).max(t)),
|
||||
(None, Some(d), None) => Some(d / 2),
|
||||
(None, None, Some(t)) => Some(t),
|
||||
(None, None, None) => None,
|
||||
};
|
||||
|
||||
base_score.map(|s| {
|
||||
let frecency_score = frecency.get_score(&item.id);
|
||||
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
|
||||
(item.clone(), s + frecency_boost)
|
||||
})
|
||||
})
|
||||
base_score.map(|s| {
|
||||
let frecency_score = frecency.get_score(&item.id);
|
||||
let frecency_boost = (frecency_score * frecency_weight * 10.0) as i64;
|
||||
(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.truncate(max_results);
|
||||
|
||||
@@ -520,7 +534,11 @@ impl ProviderManager {
|
||||
/// Get all available provider types (for UI tabs)
|
||||
#[allow(dead_code)]
|
||||
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")
|
||||
@@ -560,6 +578,16 @@ impl ProviderManager {
|
||||
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
|
||||
for provider in &self.dynamic_providers {
|
||||
if provider.type_id() == plugin_id {
|
||||
@@ -580,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")]
|
||||
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use log::debug;
|
||||
use owlry_plugin_api::{PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind};
|
||||
use owlry_plugin_api::{PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition};
|
||||
|
||||
use super::{LaunchItem, Provider, ProviderType};
|
||||
use crate::plugins::native_loader::NativePlugin;
|
||||
@@ -42,6 +42,12 @@ impl NativeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ProviderType for this native provider
|
||||
/// All native plugins return Plugin(type_id) - the core has no hardcoded plugin types
|
||||
fn get_provider_type(&self) -> ProviderType {
|
||||
ProviderType::Plugin(self.info.type_id.to_string())
|
||||
}
|
||||
|
||||
/// Convert a plugin API item to a core LaunchItem
|
||||
fn convert_item(&self, item: ApiPluginItem) -> LaunchItem {
|
||||
LaunchItem {
|
||||
@@ -49,7 +55,7 @@ impl NativeProvider {
|
||||
name: item.name.to_string(),
|
||||
description: item.description.as_ref().map(|s| s.to_string()).into(),
|
||||
icon: item.icon.as_ref().map(|s| s.to_string()).into(),
|
||||
provider: ProviderType::Plugin(self.info.type_id.to_string()),
|
||||
provider: self.get_provider_type(),
|
||||
command: item.command.to_string(),
|
||||
terminal: item.terminal,
|
||||
tags: item.keywords.iter().map(|s| s.to_string()).collect(),
|
||||
@@ -99,6 +105,17 @@ impl NativeProvider {
|
||||
self.info.type_id.as_str()
|
||||
}
|
||||
|
||||
/// Check if this is a widget provider (appears at top of results)
|
||||
pub fn is_widget(&self) -> bool {
|
||||
self.info.position == ProviderPosition::Widget
|
||||
}
|
||||
|
||||
/// Get the provider's priority for result ordering
|
||||
/// Higher values appear first in results
|
||||
pub fn priority(&self) -> i32 {
|
||||
self.info.priority
|
||||
}
|
||||
|
||||
/// Execute an action command on the provider
|
||||
/// Uses query with "!" prefix to trigger action handling in the plugin
|
||||
pub fn execute_action(&self, action: &str) {
|
||||
@@ -113,7 +130,7 @@ impl Provider for NativeProvider {
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Plugin(self.info.type_id.to_string())
|
||||
self.get_provider_type()
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
|
||||
@@ -67,6 +67,18 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Symbolic icons - inherit text color */
|
||||
.owlry-symbolic-icon {
|
||||
-gtk-icon-style: symbolic;
|
||||
}
|
||||
|
||||
/* Emoji icon - displayed as large text */
|
||||
.owlry-emoji-icon {
|
||||
font-size: 24px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* Result name */
|
||||
.owlry-result-name {
|
||||
font-size: var(--owlry-font-size, 14px);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#e0e0e0">
|
||||
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 188 B After Width: | Height: | Size: 183 B |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M3.89,17.6c0-0.99,0.31-1.88,0.93-2.65s1.41-1.27,2.38-1.49c0.26-1.17,0.85-2.14,1.78-2.88c0.93-0.75,2-1.12,3.22-1.12
|
||||
c1.18,0,2.24,0.36,3.16,1.09c0.93,0.73,1.53,1.66,1.8,2.8h0.27c1.18,0,2.18,0.41,3.01,1.24s1.25,1.83,1.25,3
|
||||
c0,1.18-0.42,2.18-1.25,3.01s-1.83,1.25-3.01,1.25H8.16c-0.58,0-1.13-0.11-1.65-0.34S5.52,21,5.14,20.62
|
||||
|
||||
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M1.56,16.9c0,0.9,0.22,1.73,0.66,2.49s1.04,1.36,1.8,1.8c0.76,0.44,1.58,0.66,2.47,0.66h10.83c0.89,0,1.72-0.22,2.48-0.66
|
||||
c0.76-0.44,1.37-1.04,1.81-1.8c0.44-0.76,0.67-1.59,0.67-2.49c0-0.66-0.14-1.33-0.42-2C22.62,13.98,23,12.87,23,11.6
|
||||
c0-0.71-0.14-1.39-0.41-2.04c-0.27-0.65-0.65-1.2-1.12-1.67C21,7.42,20.45,7.04,19.8,6.77c-0.65-0.28-1.33-0.41-2.04-0.41
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M4.37,14.62c0-0.24,0.08-0.45,0.25-0.62c0.17-0.16,0.38-0.24,0.6-0.24h2.04c0.23,0,0.42,0.08,0.58,0.25
|
||||
c0.15,0.17,0.23,0.37,0.23,0.61S8,15.06,7.85,15.23c-0.15,0.17-0.35,0.25-0.58,0.25H5.23c-0.23,0-0.43-0.08-0.6-0.25
|
||||
C4.46,15.06,4.37,14.86,4.37,14.62z M7.23,21.55c0-0.23,0.08-0.43,0.23-0.61l1.47-1.43c0.15-0.16,0.35-0.23,0.59-0.23
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M2.62,21.05c0-0.24,0.08-0.45,0.25-0.61c0.17-0.16,0.38-0.24,0.63-0.24h18.67c0.25,0,0.45,0.08,0.61,0.24
|
||||
c0.16,0.16,0.24,0.36,0.24,0.61c0,0.23-0.08,0.43-0.25,0.58c-0.17,0.16-0.37,0.23-0.6,0.23H3.5c-0.25,0-0.46-0.08-0.63-0.23
|
||||
C2.7,21.47,2.62,21.28,2.62,21.05z M5.24,17.91c0-0.24,0.09-0.44,0.26-0.6c0.15-0.15,0.35-0.23,0.59-0.23h18.67
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M7.91,14.48c0-0.96,0.19-1.87,0.56-2.75s0.88-1.63,1.51-2.26c0.63-0.63,1.39-1.14,2.27-1.52c0.88-0.38,1.8-0.57,2.75-0.57
|
||||
h1.14c0.16,0.04,0.23,0.14,0.23,0.28l0.05,0.88c0.04,1.27,0.49,2.35,1.37,3.24c0.88,0.89,1.94,1.37,3.19,1.42l0.82,0.07
|
||||
c0.16,0,0.24,0.08,0.24,0.23v0.98c0.01,1.28-0.3,2.47-0.93,3.56c-0.63,1.09-1.48,1.95-2.57,2.59c-1.08,0.63-2.27,0.95-3.55,0.95
|
||||
|
||||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M4.64,16.91c0-1.15,0.36-2.17,1.08-3.07c0.72-0.9,1.63-1.47,2.73-1.73c0.31-1.36,1.02-2.48,2.11-3.36s2.34-1.31,3.75-1.31
|
||||
c1.38,0,2.6,0.43,3.68,1.28c1.08,0.85,1.78,1.95,2.1,3.29h0.32c0.89,0,1.72,0.22,2.48,0.65s1.37,1.03,1.81,1.78
|
||||
c0.44,0.75,0.67,1.58,0.67,2.47c0,0.88-0.21,1.69-0.63,2.44c-0.42,0.75-1,1.35-1.73,1.8c-0.73,0.45-1.53,0.69-2.4,0.71
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M4.64,16.95c0-1.16,0.35-2.18,1.06-3.08s1.62-1.48,2.74-1.76c0.31-1.36,1.01-2.48,2.1-3.36s2.34-1.31,3.75-1.31
|
||||
c1.38,0,2.6,0.43,3.68,1.28c1.08,0.85,1.78,1.95,2.1,3.29h0.32c0.89,0,1.72,0.22,2.48,0.66c0.76,0.44,1.37,1.04,1.81,1.8
|
||||
c0.44,0.76,0.67,1.59,0.67,2.48c0,1.32-0.46,2.47-1.39,3.42c-0.92,0.96-2.05,1.46-3.38,1.5c-0.13,0-0.2-0.06-0.2-0.17v-1.33
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M9.91,19.56c0-0.85,0.2-1.64,0.59-2.38s0.94-1.35,1.65-1.84V5.42c0-0.8,0.27-1.48,0.82-2.03S14.2,2.55,15,2.55
|
||||
c0.81,0,1.49,0.28,2.04,0.83c0.55,0.56,0.83,1.23,0.83,2.03v9.92c0.71,0.49,1.25,1.11,1.64,1.84s0.58,1.53,0.58,2.38
|
||||
c0,0.92-0.23,1.78-0.68,2.56s-1.07,1.4-1.85,1.85s-1.63,0.68-2.56,0.68c-0.92,0-1.77-0.23-2.55-0.68s-1.4-1.07-1.86-1.85
|
||||
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
viewBox="0 0 30 30" fill="#e0e0e0" style="enable-background:new 0 0 30 30;" xml:space="preserve">
|
||||
<path d="M4.63,16.91c0,1.11,0.33,2.1,0.99,2.97s1.52,1.47,2.58,1.79l-0.66,1.68c-0.03,0.14,0.02,0.22,0.14,0.22h2.13l-0.98,4.3h0.28
|
||||
l3.92-5.75c0.04-0.04,0.04-0.09,0.01-0.14c-0.03-0.05-0.08-0.07-0.15-0.07h-2.18l2.48-4.64c0.07-0.14,0.02-0.22-0.14-0.22h-2.94
|
||||
c-0.09,0-0.17,0.05-0.23,0.15l-1.07,2.87c-0.71-0.18-1.3-0.57-1.77-1.16c-0.47-0.59-0.7-1.26-0.7-2.01c0-0.83,0.28-1.55,0.85-2.17
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -72,6 +72,17 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
|
||||
css.push_str(&format!(" --owlry-badge-web: {};\n", badge_web));
|
||||
}
|
||||
|
||||
// Widget badge colors
|
||||
if let Some(ref badge_media) = config.colors.badge_media {
|
||||
css.push_str(&format!(" --owlry-badge-media: {};\n", badge_media));
|
||||
}
|
||||
if let Some(ref badge_weather) = config.colors.badge_weather {
|
||||
css.push_str(&format!(" --owlry-badge-weather: {};\n", badge_weather));
|
||||
}
|
||||
if let Some(ref badge_pomo) = config.colors.badge_pomo {
|
||||
css.push_str(&format!(" --owlry-badge-pomo: {};\n", badge_pomo));
|
||||
}
|
||||
|
||||
css.push_str("}\n");
|
||||
css
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ struct SubmenuState {
|
||||
saved_search: String,
|
||||
}
|
||||
|
||||
/// State for lazy loading results
|
||||
#[derive(Default)]
|
||||
struct LazyLoadState {
|
||||
/// All matching results (may be more than displayed)
|
||||
all_results: Vec<LaunchItem>,
|
||||
/// Number of items currently displayed
|
||||
displayed_count: usize,
|
||||
}
|
||||
|
||||
/// Number of items to display initially and per batch
|
||||
const INITIAL_RESULTS: usize = 15;
|
||||
const LOAD_MORE_BATCH: usize = 10;
|
||||
/// Debounce delay for search input (milliseconds)
|
||||
const SEARCH_DEBOUNCE_MS: u64 = 50;
|
||||
|
||||
pub struct MainWindow {
|
||||
window: ApplicationWindow,
|
||||
search_entry: Entry,
|
||||
@@ -51,6 +66,15 @@ pub struct MainWindow {
|
||||
submenu_state: Rc<RefCell<SubmenuState>>,
|
||||
/// Parsed tab config (ProviderTypes for cycling)
|
||||
tab_order: Rc<Vec<ProviderType>>,
|
||||
/// Custom prompt text (overrides dynamic placeholder when set)
|
||||
#[allow(dead_code)]
|
||||
custom_prompt: Option<String>,
|
||||
/// Lazy loading state
|
||||
lazy_state: Rc<RefCell<LazyLoadState>>,
|
||||
/// Debounce source ID for cancelling pending searches
|
||||
debounce_source: Rc<RefCell<Option<gtk4::glib::SourceId>>>,
|
||||
/// Whether we're in dmenu mode (stdin pipe input)
|
||||
is_dmenu_mode: bool,
|
||||
}
|
||||
|
||||
impl MainWindow {
|
||||
@@ -60,6 +84,7 @@ impl MainWindow {
|
||||
providers: Rc<RefCell<ProviderManager>>,
|
||||
frecency: Rc<RefCell<FrecencyStore>>,
|
||||
filter: Rc<RefCell<ProviderFilter>>,
|
||||
custom_prompt: Option<String>,
|
||||
) -> Self {
|
||||
let cfg = config.borrow();
|
||||
|
||||
@@ -111,24 +136,21 @@ impl MainWindow {
|
||||
.build();
|
||||
filter_tabs.add_css_class("owlry-filter-tabs");
|
||||
|
||||
// Parse tabs config to ProviderTypes
|
||||
let tab_order: Vec<ProviderType> = cfg
|
||||
.general
|
||||
.tabs
|
||||
.iter()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let tab_order = Rc::new(tab_order);
|
||||
// Get enabled providers from filter (which respects CLI --mode/--providers or config)
|
||||
// This makes tabs dynamic based on what's actually enabled
|
||||
let enabled = filter.borrow().enabled_providers();
|
||||
let tab_strings: Vec<String> = enabled.iter().map(|p| p.to_string()).collect();
|
||||
let tab_order = Rc::new(enabled);
|
||||
|
||||
// Create toggle buttons for each provider (from config)
|
||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &cfg.general.tabs);
|
||||
// Create toggle buttons for each enabled provider
|
||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &tab_strings);
|
||||
let filter_buttons = Rc::new(RefCell::new(filter_buttons));
|
||||
|
||||
header_box.append(&mode_label);
|
||||
header_box.append(&filter_tabs);
|
||||
|
||||
// Search entry with dynamic placeholder
|
||||
let placeholder = Self::build_placeholder(&filter.borrow());
|
||||
// Search entry with dynamic placeholder (or custom prompt if provided)
|
||||
let placeholder = custom_prompt.clone().unwrap_or_else(|| Self::build_placeholder(&filter.borrow()));
|
||||
let search_entry = Entry::builder()
|
||||
.placeholder_text(&placeholder)
|
||||
.hexpand(true)
|
||||
@@ -175,6 +197,11 @@ impl MainWindow {
|
||||
|
||||
drop(cfg);
|
||||
|
||||
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default()));
|
||||
|
||||
// Check if we're in dmenu mode (stdin pipe input)
|
||||
let is_dmenu_mode = providers.borrow().is_dmenu_mode();
|
||||
|
||||
let main_window = Self {
|
||||
window,
|
||||
search_entry,
|
||||
@@ -190,9 +217,14 @@ impl MainWindow {
|
||||
filter_buttons,
|
||||
submenu_state: Rc::new(RefCell::new(SubmenuState::default())),
|
||||
tab_order,
|
||||
custom_prompt,
|
||||
lazy_state,
|
||||
debounce_source: Rc::new(RefCell::new(None)),
|
||||
is_dmenu_mode,
|
||||
};
|
||||
|
||||
main_window.setup_signals();
|
||||
main_window.setup_lazy_loading();
|
||||
main_window.update_results("");
|
||||
|
||||
// Ensure search entry has focus when window is shown
|
||||
@@ -232,11 +264,11 @@ impl MainWindow {
|
||||
|
||||
let mut results = current_results_for_auto.borrow_mut();
|
||||
for type_id in &widget_ids {
|
||||
if let Some(new_item) = providers_for_auto.borrow().get_widget_item(type_id) {
|
||||
if let Some(existing) = results.iter_mut().find(|i| i.id == new_item.id) {
|
||||
existing.name = new_item.name;
|
||||
existing.description = new_item.description;
|
||||
}
|
||||
if let Some(new_item) = providers_for_auto.borrow().get_widget_item(type_id)
|
||||
&& let Some(existing) = results.iter_mut().find(|i| i.id == new_item.id)
|
||||
{
|
||||
existing.name = new_item.name;
|
||||
existing.description = new_item.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,11 +295,21 @@ impl MainWindow {
|
||||
}
|
||||
};
|
||||
|
||||
let label = Self::provider_tab_label(&provider_type);
|
||||
let base_label = Self::provider_tab_label(&provider_type);
|
||||
// Show number hint in the label for first 9 tabs (using superscript)
|
||||
let label = if idx < 9 {
|
||||
let superscript = match idx + 1 {
|
||||
1 => "¹", 2 => "²", 3 => "³", 4 => "⁴", 5 => "⁵",
|
||||
6 => "⁶", 7 => "⁷", 8 => "⁸", 9 => "⁹", _ => "",
|
||||
};
|
||||
format!("{}{}", base_label, superscript)
|
||||
} else {
|
||||
base_label.to_string()
|
||||
};
|
||||
let shortcut = format!("Ctrl+{}", idx + 1);
|
||||
|
||||
let button = ToggleButton::builder()
|
||||
.label(label)
|
||||
.label(&label)
|
||||
.tooltip_text(&shortcut)
|
||||
.active(filter.borrow().is_enabled(provider_type.clone()))
|
||||
.build();
|
||||
@@ -284,48 +326,54 @@ impl MainWindow {
|
||||
}
|
||||
|
||||
/// Get display label for a provider tab
|
||||
/// Core types have fixed labels; plugins derive labels from type_id
|
||||
fn provider_tab_label(provider: &ProviderType) -> &'static str {
|
||||
match provider {
|
||||
ProviderType::Application => "Apps",
|
||||
ProviderType::Bookmarks => "Bookmarks",
|
||||
ProviderType::Calculator => "Calc",
|
||||
ProviderType::Clipboard => "Clip",
|
||||
ProviderType::Command => "Cmds",
|
||||
ProviderType::Dmenu => "Dmenu",
|
||||
ProviderType::Emoji => "Emoji",
|
||||
ProviderType::Files => "Files",
|
||||
ProviderType::MediaPlayer => "Media",
|
||||
ProviderType::Pomodoro => "Pomo",
|
||||
ProviderType::Scripts => "Scripts",
|
||||
ProviderType::Ssh => "SSH",
|
||||
ProviderType::System => "System",
|
||||
ProviderType::Uuctl => "uuctl",
|
||||
ProviderType::Weather => "Weather",
|
||||
ProviderType::WebSearch => "Web",
|
||||
ProviderType::Plugin(_) => "Plugin",
|
||||
ProviderType::Plugin(type_id) => match type_id.as_str() {
|
||||
"bookmarks" => "Bookmarks",
|
||||
"calc" => "Calc",
|
||||
"clipboard" => "Clip",
|
||||
"emoji" => "Emoji",
|
||||
"filesearch" => "Files",
|
||||
"media" => "Media",
|
||||
"pomodoro" => "Pomo",
|
||||
"scripts" => "Scripts",
|
||||
"ssh" => "SSH",
|
||||
"system" => "System",
|
||||
"uuctl" => "uuctl",
|
||||
"weather" => "Weather",
|
||||
"websearch" => "Web",
|
||||
_ => "Plugin",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Get CSS class for a provider
|
||||
/// Core types have fixed CSS classes; plugins derive from type_id
|
||||
fn provider_css_class(provider: &ProviderType) -> &'static str {
|
||||
match provider {
|
||||
ProviderType::Application => "owlry-filter-app",
|
||||
ProviderType::Bookmarks => "owlry-filter-bookmark",
|
||||
ProviderType::Calculator => "owlry-filter-calc",
|
||||
ProviderType::Clipboard => "owlry-filter-clip",
|
||||
ProviderType::Command => "owlry-filter-cmd",
|
||||
ProviderType::Dmenu => "owlry-filter-dmenu",
|
||||
ProviderType::Emoji => "owlry-filter-emoji",
|
||||
ProviderType::Files => "owlry-filter-file",
|
||||
ProviderType::MediaPlayer => "owlry-filter-media",
|
||||
ProviderType::Pomodoro => "owlry-filter-pomodoro",
|
||||
ProviderType::Scripts => "owlry-filter-script",
|
||||
ProviderType::Ssh => "owlry-filter-ssh",
|
||||
ProviderType::System => "owlry-filter-sys",
|
||||
ProviderType::Uuctl => "owlry-filter-uuctl",
|
||||
ProviderType::Weather => "owlry-filter-weather",
|
||||
ProviderType::WebSearch => "owlry-filter-web",
|
||||
ProviderType::Plugin(_) => "owlry-filter-plugin",
|
||||
ProviderType::Plugin(type_id) => match type_id.as_str() {
|
||||
"bookmarks" => "owlry-filter-bookmark",
|
||||
"calc" => "owlry-filter-calc",
|
||||
"clipboard" => "owlry-filter-clip",
|
||||
"emoji" => "owlry-filter-emoji",
|
||||
"filesearch" => "owlry-filter-file",
|
||||
"media" => "owlry-filter-media",
|
||||
"pomodoro" => "owlry-filter-pomodoro",
|
||||
"scripts" => "owlry-filter-script",
|
||||
"ssh" => "owlry-filter-ssh",
|
||||
"system" => "owlry-filter-sys",
|
||||
"uuctl" => "owlry-filter-uuctl",
|
||||
"weather" => "owlry-filter-weather",
|
||||
"websearch" => "owlry-filter-web",
|
||||
_ => "owlry-filter-plugin",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,22 +383,24 @@ impl MainWindow {
|
||||
.iter()
|
||||
.map(|p| match p {
|
||||
ProviderType::Application => "applications",
|
||||
ProviderType::Bookmarks => "bookmarks",
|
||||
ProviderType::Calculator => "calculator",
|
||||
ProviderType::Clipboard => "clipboard",
|
||||
ProviderType::Command => "commands",
|
||||
ProviderType::Dmenu => "options",
|
||||
ProviderType::Emoji => "emoji",
|
||||
ProviderType::Files => "files",
|
||||
ProviderType::MediaPlayer => "media",
|
||||
ProviderType::Pomodoro => "pomodoro",
|
||||
ProviderType::Scripts => "scripts",
|
||||
ProviderType::Ssh => "SSH hosts",
|
||||
ProviderType::System => "system",
|
||||
ProviderType::Uuctl => "uuctl units",
|
||||
ProviderType::Weather => "weather",
|
||||
ProviderType::WebSearch => "web",
|
||||
ProviderType::Plugin(_) => "plugins",
|
||||
ProviderType::Plugin(type_id) => match type_id.as_str() {
|
||||
"bookmarks" => "bookmarks",
|
||||
"calc" => "calculator",
|
||||
"clipboard" => "clipboard",
|
||||
"emoji" => "emoji",
|
||||
"filesearch" => "files",
|
||||
"media" => "media",
|
||||
"pomodoro" => "pomodoro",
|
||||
"scripts" => "scripts",
|
||||
"ssh" => "SSH hosts",
|
||||
"system" => "system",
|
||||
"uuctl" => "uuctl units",
|
||||
"weather" => "weather",
|
||||
"websearch" => "web",
|
||||
_ => "plugins",
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -515,7 +565,7 @@ impl MainWindow {
|
||||
}
|
||||
|
||||
fn setup_signals(&self) {
|
||||
// Search input handling with prefix detection
|
||||
// Search input handling with prefix detection and debouncing
|
||||
let providers = self.providers.clone();
|
||||
let results_list = self.results_list.clone();
|
||||
let config = self.config.clone();
|
||||
@@ -525,11 +575,13 @@ impl MainWindow {
|
||||
let mode_label = self.mode_label.clone();
|
||||
let search_entry_for_change = self.search_entry.clone();
|
||||
let submenu_state = self.submenu_state.clone();
|
||||
let lazy_state = self.lazy_state.clone();
|
||||
let debounce_source = self.debounce_source.clone();
|
||||
|
||||
self.search_entry.connect_changed(move |entry| {
|
||||
let raw_query = entry.text();
|
||||
|
||||
// If in submenu, filter the submenu items
|
||||
// If in submenu, filter immediately (no debounce needed for small local lists)
|
||||
if submenu_state.borrow().active {
|
||||
let state = submenu_state.borrow();
|
||||
let query = raw_query.to_lowercase();
|
||||
@@ -567,7 +619,7 @@ impl MainWindow {
|
||||
return;
|
||||
}
|
||||
|
||||
// Normal mode: parse prefix and search
|
||||
// Normal mode: update prefix/UI immediately for responsiveness
|
||||
let parsed = ProviderFilter::parse_query(&raw_query);
|
||||
|
||||
{
|
||||
@@ -580,63 +632,102 @@ impl MainWindow {
|
||||
if let Some(ref prefix) = parsed.prefix {
|
||||
let prefix_name = match prefix {
|
||||
ProviderType::Application => "applications",
|
||||
ProviderType::Bookmarks => "bookmarks",
|
||||
ProviderType::Calculator => "calculator",
|
||||
ProviderType::Clipboard => "clipboard",
|
||||
ProviderType::Command => "commands",
|
||||
ProviderType::Dmenu => "options",
|
||||
ProviderType::Emoji => "emoji",
|
||||
ProviderType::Files => "files",
|
||||
ProviderType::MediaPlayer => "media",
|
||||
ProviderType::Pomodoro => "pomodoro",
|
||||
ProviderType::Scripts => "scripts",
|
||||
ProviderType::Ssh => "SSH hosts",
|
||||
ProviderType::System => "system",
|
||||
ProviderType::Uuctl => "uuctl units",
|
||||
ProviderType::Weather => "weather",
|
||||
ProviderType::WebSearch => "web",
|
||||
ProviderType::Plugin(_) => "plugins",
|
||||
ProviderType::Plugin(type_id) => match type_id.as_str() {
|
||||
"bookmarks" => "bookmarks",
|
||||
"calc" => "calculator",
|
||||
"clipboard" => "clipboard",
|
||||
"emoji" => "emoji",
|
||||
"filesearch" => "files",
|
||||
"media" => "media",
|
||||
"pomodoro" => "pomodoro",
|
||||
"scripts" => "scripts",
|
||||
"ssh" => "SSH hosts",
|
||||
"system" => "system",
|
||||
"uuctl" => "uuctl units",
|
||||
"weather" => "weather",
|
||||
"websearch" => "web",
|
||||
_ => "plugins",
|
||||
},
|
||||
};
|
||||
search_entry_for_change
|
||||
.set_placeholder_text(Some(&format!("Search {}...", prefix_name)));
|
||||
}
|
||||
|
||||
let cfg = config.borrow();
|
||||
let max_results = cfg.general.max_results;
|
||||
let frecency_weight = cfg.providers.frecency_weight;
|
||||
let use_frecency = cfg.providers.frecency;
|
||||
drop(cfg);
|
||||
|
||||
let results: Vec<LaunchItem> = if use_frecency {
|
||||
providers
|
||||
.borrow_mut()
|
||||
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
} else {
|
||||
providers
|
||||
.borrow()
|
||||
.search_filtered(&parsed.query, max_results, &filter.borrow())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
};
|
||||
|
||||
while let Some(child) = results_list.first_child() {
|
||||
results_list.remove(&child);
|
||||
// Cancel any pending debounced search
|
||||
if let Some(source_id) = debounce_source.borrow_mut().take() {
|
||||
source_id.remove();
|
||||
}
|
||||
|
||||
for item in &results {
|
||||
let row = ResultRow::new(item);
|
||||
results_list.append(&row);
|
||||
}
|
||||
// Clone references for the debounced closure
|
||||
let providers = providers.clone();
|
||||
let results_list = results_list.clone();
|
||||
let config = config.clone();
|
||||
let frecency = frecency.clone();
|
||||
let current_results = current_results.clone();
|
||||
let filter = filter.clone();
|
||||
let lazy_state = lazy_state.clone();
|
||||
let debounce_source_for_closure = debounce_source.clone();
|
||||
|
||||
if let Some(first_row) = results_list.row_at_index(0) {
|
||||
results_list.select_row(Some(&first_row));
|
||||
}
|
||||
// Schedule debounced search
|
||||
let source_id = gtk4::glib::timeout_add_local_once(
|
||||
std::time::Duration::from_millis(SEARCH_DEBOUNCE_MS),
|
||||
move || {
|
||||
// Clear the source ID since we're now executing
|
||||
*debounce_source_for_closure.borrow_mut() = None;
|
||||
|
||||
*current_results.borrow_mut() = results;
|
||||
let cfg = config.borrow();
|
||||
let max_results = cfg.general.max_results;
|
||||
let frecency_weight = cfg.providers.frecency_weight;
|
||||
let use_frecency = cfg.providers.frecency;
|
||||
drop(cfg);
|
||||
|
||||
let results: Vec<LaunchItem> = if use_frecency {
|
||||
providers
|
||||
.borrow_mut()
|
||||
.search_with_frecency(&parsed.query, max_results, &filter.borrow(), &frecency.borrow(), frecency_weight, parsed.tag_filter.as_deref())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
} else {
|
||||
providers
|
||||
.borrow()
|
||||
.search_filtered(&parsed.query, max_results, &filter.borrow())
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Clear existing results
|
||||
while let Some(child) = results_list.first_child() {
|
||||
results_list.remove(&child);
|
||||
}
|
||||
|
||||
// Lazy loading: store all results but only display initial batch
|
||||
let initial_count = INITIAL_RESULTS.min(results.len());
|
||||
{
|
||||
let mut lazy = lazy_state.borrow_mut();
|
||||
lazy.all_results = results.clone();
|
||||
lazy.displayed_count = initial_count;
|
||||
}
|
||||
|
||||
// Display only initial batch
|
||||
for item in results.iter().take(initial_count) {
|
||||
let row = ResultRow::new(item);
|
||||
results_list.append(&row);
|
||||
}
|
||||
|
||||
if let Some(first_row) = results_list.row_at_index(0) {
|
||||
results_list.select_row(Some(&first_row));
|
||||
}
|
||||
|
||||
// current_results holds only what's displayed (for selection/activation)
|
||||
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
|
||||
},
|
||||
);
|
||||
|
||||
*debounce_source.borrow_mut() = Some(source_id);
|
||||
});
|
||||
|
||||
// Entry activate signal (Enter key in search entry)
|
||||
@@ -650,12 +741,14 @@ impl MainWindow {
|
||||
let mode_label_for_activate = self.mode_label.clone();
|
||||
let hints_label_for_activate = self.hints_label.clone();
|
||||
let search_entry_for_activate = self.search_entry.clone();
|
||||
let is_dmenu_mode_for_activate = self.is_dmenu_mode;
|
||||
|
||||
self.search_entry.connect_activate(move |entry| {
|
||||
let selected = results_list_for_activate
|
||||
.selected_row()
|
||||
.or_else(|| results_list_for_activate.row_at_index(0));
|
||||
|
||||
// Handle the case where we have a selected item
|
||||
if let Some(row) = selected {
|
||||
let index = row.index() as usize;
|
||||
let results = current_results_for_activate.borrow();
|
||||
@@ -702,6 +795,10 @@ impl MainWindow {
|
||||
&providers_for_activate,
|
||||
);
|
||||
if should_close {
|
||||
// In dmenu mode, exit with success code
|
||||
if is_dmenu_mode_for_activate {
|
||||
std::process::exit(0);
|
||||
}
|
||||
window_for_activate.close();
|
||||
} else {
|
||||
// Trigger search refresh for updated widget state
|
||||
@@ -709,6 +806,16 @@ impl MainWindow {
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No item selected/matched - in dmenu mode, output the typed text
|
||||
if is_dmenu_mode_for_activate {
|
||||
let text = entry.text();
|
||||
if !text.is_empty() {
|
||||
println!("{}", text);
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -749,6 +856,7 @@ impl MainWindow {
|
||||
let hints_label = self.hints_label.clone();
|
||||
let submenu_state = self.submenu_state.clone();
|
||||
let tab_order = self.tab_order.clone();
|
||||
let is_dmenu_mode = self.is_dmenu_mode;
|
||||
|
||||
key_controller.connect_key_pressed(move |_, key, _, modifiers| {
|
||||
let ctrl = modifiers.contains(gtk4::gdk::ModifierType::CONTROL_MASK);
|
||||
@@ -771,6 +879,10 @@ impl MainWindow {
|
||||
);
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
// In dmenu mode, exit with cancel code (1)
|
||||
if is_dmenu_mode {
|
||||
std::process::exit(1);
|
||||
}
|
||||
window.close();
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
@@ -788,6 +900,10 @@ impl MainWindow {
|
||||
);
|
||||
gtk4::glib::Propagation::Stop
|
||||
} else {
|
||||
// In dmenu mode, exit with cancel code (1)
|
||||
if is_dmenu_mode {
|
||||
std::process::exit(1);
|
||||
}
|
||||
window.close();
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
@@ -847,6 +963,7 @@ impl MainWindow {
|
||||
// Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu)
|
||||
Key::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 |
|
||||
Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => {
|
||||
info!("[UI] Ctrl+number detected: {:?}", key);
|
||||
if !submenu_state.borrow().active {
|
||||
let idx = match key {
|
||||
Key::_1 => 0,
|
||||
@@ -860,7 +977,9 @@ impl MainWindow {
|
||||
Key::_9 => 8,
|
||||
_ => return gtk4::glib::Propagation::Proceed,
|
||||
};
|
||||
info!("[UI] Toggling tab at index {}", idx);
|
||||
if let Some(provider) = tab_order.get(idx) {
|
||||
info!("[UI] Found provider: {:?}", provider);
|
||||
Self::toggle_provider_button(
|
||||
provider.clone(),
|
||||
&filter,
|
||||
@@ -868,6 +987,8 @@ impl MainWindow {
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
);
|
||||
} else {
|
||||
info!("[UI] No provider at index {}, tab_order len={}", idx, tab_order.len());
|
||||
}
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
@@ -955,25 +1076,65 @@ impl MainWindow {
|
||||
}
|
||||
|
||||
let current = filter.borrow().enabled_providers();
|
||||
let all_enabled = current.len() == tab_order.len();
|
||||
|
||||
let next = if current.len() == 1 {
|
||||
let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
||||
if forward {
|
||||
tab_order[(idx + 1) % tab_order.len()].clone()
|
||||
// Cycle: All -> Provider1 -> Provider2 -> ... -> ProviderN -> All
|
||||
// In "All" mode (all providers enabled), we go to first provider (forward) or last (backward)
|
||||
// In single-provider mode, we go to next provider or back to All at the boundary
|
||||
if all_enabled {
|
||||
// Currently showing all, go to first (forward) or last (backward) single provider
|
||||
let next = if forward {
|
||||
tab_order[0].clone()
|
||||
} else {
|
||||
tab_order[(idx + tab_order.len() - 1) % tab_order.len()].clone()
|
||||
tab_order[tab_order.len() - 1].clone()
|
||||
};
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
}
|
||||
} else if current.len() == 1 {
|
||||
let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
||||
let at_boundary = if forward { idx == tab_order.len() - 1 } else { idx == 0 };
|
||||
|
||||
if at_boundary {
|
||||
// At boundary, go back to "All" mode
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
for provider in tab_order {
|
||||
f.enable(provider.clone());
|
||||
}
|
||||
}
|
||||
for (_, button) in buttons.borrow().iter() {
|
||||
button.set_active(true);
|
||||
}
|
||||
} else {
|
||||
// Move to next/previous provider
|
||||
let next = if forward {
|
||||
tab_order[idx + 1].clone()
|
||||
} else {
|
||||
tab_order[idx - 1].clone()
|
||||
};
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tab_order[0].clone()
|
||||
};
|
||||
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
// Some but not all providers enabled - go to first provider
|
||||
let next = tab_order[0].clone();
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
}
|
||||
}
|
||||
|
||||
mode_label.set_label(filter.borrow().mode_display_name());
|
||||
@@ -1009,6 +1170,7 @@ impl MainWindow {
|
||||
let use_frecency = cfg.providers.frecency;
|
||||
drop(cfg);
|
||||
|
||||
// Fetch all matching results (up to max_results)
|
||||
let results: Vec<LaunchItem> = if use_frecency {
|
||||
self.providers
|
||||
.borrow_mut()
|
||||
@@ -1025,11 +1187,21 @@ impl MainWindow {
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Clear existing results
|
||||
while let Some(child) = self.results_list.first_child() {
|
||||
self.results_list.remove(&child);
|
||||
}
|
||||
|
||||
for item in &results {
|
||||
// Store all results for lazy loading
|
||||
let initial_count = INITIAL_RESULTS.min(results.len());
|
||||
{
|
||||
let mut lazy = self.lazy_state.borrow_mut();
|
||||
lazy.all_results = results.clone();
|
||||
lazy.displayed_count = initial_count;
|
||||
}
|
||||
|
||||
// Display initial batch only
|
||||
for item in results.iter().take(initial_count) {
|
||||
let row = ResultRow::new(item);
|
||||
self.results_list.append(&row);
|
||||
}
|
||||
@@ -1038,7 +1210,74 @@ impl MainWindow {
|
||||
self.results_list.select_row(Some(&first_row));
|
||||
}
|
||||
|
||||
*self.current_results.borrow_mut() = results;
|
||||
// current_results holds what's currently displayed
|
||||
*self.current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
|
||||
}
|
||||
|
||||
/// Set up lazy loading scroll detection
|
||||
fn setup_lazy_loading(&self) {
|
||||
let vadj = self.scrolled.vadjustment();
|
||||
let results_list = self.results_list.clone();
|
||||
let lazy_state = self.lazy_state.clone();
|
||||
let current_results = self.current_results.clone();
|
||||
|
||||
// Load more on scroll
|
||||
vadj.connect_value_changed(move |adj| {
|
||||
let value = adj.value();
|
||||
let upper = adj.upper();
|
||||
let page_size = adj.page_size();
|
||||
|
||||
// Load more when near bottom (within 50px)
|
||||
let near_bottom = upper > page_size && (value + page_size >= upper - 50.0);
|
||||
if near_bottom {
|
||||
Self::load_more_items(&lazy_state, &results_list, ¤t_results);
|
||||
}
|
||||
});
|
||||
|
||||
// Also load more when selecting rows near the end (keyboard navigation)
|
||||
let lazy_state2 = self.lazy_state.clone();
|
||||
let results_list2 = self.results_list.clone();
|
||||
let current_results2 = self.current_results.clone();
|
||||
|
||||
self.results_list.connect_row_selected(move |_, row| {
|
||||
if let Some(row) = row {
|
||||
let index = row.index();
|
||||
let lazy = lazy_state2.borrow();
|
||||
let displayed = lazy.displayed_count;
|
||||
let all_count = lazy.all_results.len();
|
||||
drop(lazy);
|
||||
|
||||
// Load more if within 3 items of the end
|
||||
if displayed < all_count && (index as usize) >= displayed.saturating_sub(3) {
|
||||
Self::load_more_items(&lazy_state2, &results_list2, ¤t_results2);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Load more items from lazy state
|
||||
fn load_more_items(
|
||||
lazy_state: &Rc<RefCell<LazyLoadState>>,
|
||||
results_list: &ListBox,
|
||||
current_results: &Rc<RefCell<Vec<LaunchItem>>>,
|
||||
) {
|
||||
let mut lazy = lazy_state.borrow_mut();
|
||||
let all_count = lazy.all_results.len();
|
||||
let displayed = lazy.displayed_count;
|
||||
|
||||
if displayed < all_count {
|
||||
// Load next batch
|
||||
let new_end = (displayed + LOAD_MORE_BATCH).min(all_count);
|
||||
for item in lazy.all_results[displayed..new_end].iter() {
|
||||
let row = ResultRow::new(item);
|
||||
results_list.append(&row);
|
||||
}
|
||||
lazy.displayed_count = new_end;
|
||||
|
||||
// Update current_results
|
||||
let mut current = current_results.borrow_mut();
|
||||
current.extend(lazy.all_results[displayed..new_end].iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle item activation - returns true if window should close
|
||||
@@ -1062,6 +1301,12 @@ impl MainWindow {
|
||||
}
|
||||
|
||||
fn launch_item(item: &LaunchItem, config: &Config, frecency: &Rc<RefCell<FrecencyStore>>) {
|
||||
// dmenu mode: print selection to stdout instead of executing
|
||||
if matches!(item.provider, ProviderType::Dmenu) {
|
||||
println!("{}", item.name);
|
||||
return;
|
||||
}
|
||||
|
||||
// Record this launch for frecency tracking
|
||||
if config.providers.frecency {
|
||||
frecency.borrow_mut().record_launch(&item.id);
|
||||
@@ -1072,17 +1317,89 @@ impl MainWindow {
|
||||
info!("Launching: {} ({})", item.name, item.command);
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider);
|
||||
debug!("[UI] Launch details: terminal={}, provider={:?}, id={}", item.terminal, item.provider, item.id);
|
||||
|
||||
let cmd = if item.terminal {
|
||||
format!("{} -e {}", config.general.terminal_command, item.command)
|
||||
// Check if this is a desktop application (has .desktop file as ID)
|
||||
let is_desktop_app = matches!(item.provider, ProviderType::Application)
|
||||
&& item.id.ends_with(".desktop");
|
||||
|
||||
// Desktop files should be launched via proper launchers that implement the
|
||||
// freedesktop Desktop Entry spec (D-Bus activation, field codes, env vars, etc.)
|
||||
// We delegate to: uwsm (if configured), gio launch, or gtk-launch as fallback.
|
||||
//
|
||||
// Non-desktop items (commands, plugins) use sh -c for shell execution.
|
||||
let result = if is_desktop_app {
|
||||
Self::launch_desktop_file(&item.id, config)
|
||||
} else {
|
||||
item.command.clone()
|
||||
Self::launch_command(&item.command, item.terminal, config)
|
||||
};
|
||||
|
||||
// Detect if this is a shell command vs an application launch
|
||||
// Shell commands: playerctl, dbus-send, systemctl, journalctl, or anything with shell operators
|
||||
let is_shell_command = cmd.starts_with("playerctl ")
|
||||
if let Err(e) = result {
|
||||
let msg = format!("Failed to launch '{}': {}", item.name, e);
|
||||
log::error!("{}", msg);
|
||||
crate::notify::notify("Launch failed", &msg);
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch a .desktop file.
|
||||
///
|
||||
/// When `use_uwsm` is enabled in config, launches via `uwsm app -- <file>`
|
||||
/// which starts the app in a proper systemd user session.
|
||||
///
|
||||
/// Otherwise, uses `gio launch` which is always available (part of glib2/GTK4)
|
||||
/// and handles D-Bus activation, field codes, Terminal flag, etc.
|
||||
fn launch_desktop_file(desktop_path: &str, config: &Config) -> std::io::Result<std::process::Child> {
|
||||
use std::path::Path;
|
||||
|
||||
// Check if desktop file exists
|
||||
if !Path::new(desktop_path).exists() {
|
||||
let msg = format!("Desktop file not found: {}", desktop_path);
|
||||
log::error!("{}", msg);
|
||||
crate::notify::notify("Launch failed", &msg);
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg));
|
||||
}
|
||||
|
||||
if config.general.use_uwsm {
|
||||
// Check if uwsm is available
|
||||
let uwsm_available = Command::new("which")
|
||||
.arg("uwsm")
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if !uwsm_available {
|
||||
let msg = "uwsm is enabled in config but not installed";
|
||||
log::error!("{}", msg);
|
||||
crate::notify::notify("Launch failed", msg);
|
||||
return Err(std::io::Error::new(std::io::ErrorKind::NotFound, msg));
|
||||
}
|
||||
|
||||
info!("Launching via uwsm: {}", desktop_path);
|
||||
Command::new("uwsm")
|
||||
.args(["app", "--", desktop_path])
|
||||
.spawn()
|
||||
} else {
|
||||
info!("Launching via gio: {}", desktop_path);
|
||||
Command::new("gio")
|
||||
.args(["launch", desktop_path])
|
||||
.spawn()
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch a shell command (for non-desktop items like PATH commands, plugins, etc.)
|
||||
fn launch_command(command: &str, terminal: bool, config: &Config) -> std::io::Result<std::process::Child> {
|
||||
let cmd = if terminal {
|
||||
let terminal_cmd = config.general.terminal_command.as_deref().unwrap_or("xterm");
|
||||
format!("{} -e {}", terminal_cmd, command)
|
||||
} else {
|
||||
command.to_string()
|
||||
};
|
||||
|
||||
// Shell/system commands run directly without uwsm wrapper
|
||||
// (they're typically short-lived or system utilities)
|
||||
let is_system_command = cmd.starts_with("playerctl ")
|
||||
|| cmd.starts_with("dbus-send ")
|
||||
|| cmd.starts_with("systemctl ")
|
||||
|| cmd.starts_with("journalctl ")
|
||||
@@ -1092,28 +1409,14 @@ impl MainWindow {
|
||||
|| cmd.contains(" > ")
|
||||
|| cmd.contains(" < ");
|
||||
|
||||
// Use launch wrapper if configured (uwsm, hyprctl, etc.)
|
||||
// But skip wrapper for shell commands - they need sh -c
|
||||
let result = match &config.general.launch_wrapper {
|
||||
Some(wrapper) if !wrapper.is_empty() && !is_shell_command => {
|
||||
info!("Using launch wrapper: {}", wrapper);
|
||||
// Split wrapper into command and args (e.g., "uwsm app --" -> ["uwsm", "app", "--"])
|
||||
let mut wrapper_parts: Vec<&str> = wrapper.split_whitespace().collect();
|
||||
if wrapper_parts.is_empty() {
|
||||
Command::new("sh").arg("-c").arg(&cmd).spawn()
|
||||
} else {
|
||||
let wrapper_cmd = wrapper_parts.remove(0);
|
||||
Command::new(wrapper_cmd)
|
||||
.args(&wrapper_parts)
|
||||
.arg(&cmd)
|
||||
.spawn()
|
||||
}
|
||||
}
|
||||
_ => Command::new("sh").arg("-c").arg(&cmd).spawn(),
|
||||
};
|
||||
|
||||
if let Err(e) = result {
|
||||
log::error!("Failed to launch '{}': {}", item.name, e);
|
||||
// Use uwsm for regular commands if enabled (and not a system command)
|
||||
if config.general.use_uwsm && !is_system_command {
|
||||
info!("Launching command via uwsm: {}", cmd);
|
||||
Command::new("uwsm")
|
||||
.args(["app", "--", "sh", "-c", &cmd])
|
||||
.spawn()
|
||||
} else {
|
||||
Command::new("sh").arg("-c").arg(&cmd).spawn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,17 @@ pub struct ResultRow {
|
||||
row: ListBoxRow,
|
||||
}
|
||||
|
||||
/// Check if a string looks like an emoji (starts with a non-ASCII character
|
||||
/// and is very short - typically 1-4 chars for complex emojis with ZWJ)
|
||||
fn is_emoji_icon(s: &str) -> bool {
|
||||
if s.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// Emojis are non-ASCII and typically very short (1-8 chars for complex ZWJ sequences)
|
||||
let first_char = s.chars().next().unwrap();
|
||||
!first_char.is_ascii() && s.chars().count() <= 8
|
||||
}
|
||||
|
||||
impl ResultRow {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(item: &LaunchItem) -> ListBoxRow {
|
||||
@@ -26,46 +37,60 @@ impl ResultRow {
|
||||
.margin_end(12)
|
||||
.build();
|
||||
|
||||
// Icon - handle GResource paths, file paths, icon names, and fallbacks
|
||||
// Icon - handle GResource paths, file paths, icon names, emojis, and fallbacks
|
||||
let icon_widget: Widget = if let Some(icon_path) = &item.icon {
|
||||
let img = if icon_path.starts_with("/org/owlry/launcher/icons/") {
|
||||
if is_emoji_icon(icon_path) {
|
||||
// Emoji character - display as text label
|
||||
let emoji_label = Label::builder()
|
||||
.label(icon_path)
|
||||
.width_request(32)
|
||||
.height_request(32)
|
||||
.valign(gtk4::Align::Center)
|
||||
.halign(gtk4::Align::Center)
|
||||
.build();
|
||||
emoji_label.add_css_class("owlry-result-icon");
|
||||
emoji_label.add_css_class("owlry-emoji-icon");
|
||||
emoji_label.upcast()
|
||||
} else if icon_path.starts_with("/org/owlry/launcher/icons/") {
|
||||
// GResource path - load from bundled resources
|
||||
Image::from_resource(icon_path)
|
||||
let img = Image::from_resource(icon_path);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
// SVG icons from resources should be treated as symbolic for color inheritance
|
||||
if icon_path.ends_with(".svg") {
|
||||
img.add_css_class("owlry-symbolic-icon");
|
||||
}
|
||||
img.upcast()
|
||||
} else if icon_path.starts_with('/') {
|
||||
// Absolute file path
|
||||
Image::from_file(icon_path)
|
||||
let img = Image::from_file(icon_path);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.upcast()
|
||||
} else {
|
||||
// Icon theme name
|
||||
Image::from_icon_name(icon_path)
|
||||
};
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.upcast()
|
||||
let img = Image::from_icon_name(icon_path);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
// Add symbolic class for icons ending with "-symbolic"
|
||||
if icon_path.ends_with("-symbolic") {
|
||||
img.add_css_class("owlry-symbolic-icon");
|
||||
}
|
||||
img.upcast()
|
||||
}
|
||||
} else {
|
||||
// Default icon based on provider type
|
||||
// Default icon based on provider type (only core types, plugins should provide icons)
|
||||
let default_icon = match &item.provider {
|
||||
crate::providers::ProviderType::Application => "application-x-executable",
|
||||
crate::providers::ProviderType::Bookmarks => "user-bookmarks",
|
||||
crate::providers::ProviderType::Calculator => "accessories-calculator",
|
||||
crate::providers::ProviderType::Clipboard => "edit-paste",
|
||||
crate::providers::ProviderType::Command => "utilities-terminal",
|
||||
crate::providers::ProviderType::Application => "application-x-executable-symbolic",
|
||||
crate::providers::ProviderType::Command => "utilities-terminal-symbolic",
|
||||
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
|
||||
crate::providers::ProviderType::Emoji => "face-smile",
|
||||
crate::providers::ProviderType::Files => "folder",
|
||||
crate::providers::ProviderType::Scripts => "application-x-executable",
|
||||
crate::providers::ProviderType::Ssh => "network-server",
|
||||
crate::providers::ProviderType::System => "system-shutdown",
|
||||
crate::providers::ProviderType::Uuctl => "system-run",
|
||||
crate::providers::ProviderType::WebSearch => "web-browser",
|
||||
// Widget providers now have icons set, but keep fallbacks
|
||||
crate::providers::ProviderType::Weather => "weather-clear-symbolic",
|
||||
crate::providers::ProviderType::MediaPlayer => "media-playback-start-symbolic",
|
||||
crate::providers::ProviderType::Pomodoro => "alarm-symbolic",
|
||||
crate::providers::ProviderType::Plugin(_) => "application-x-addon",
|
||||
// Plugins should provide their own icon; fallback to generic addon icon
|
||||
crate::providers::ProviderType::Plugin(_) => "application-x-addon-symbolic",
|
||||
};
|
||||
let img = Image::from_icon_name(default_icon);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.add_css_class("owlry-symbolic-icon");
|
||||
img.upcast()
|
||||
};
|
||||
|
||||
|
||||
@@ -17,22 +17,47 @@
|
||||
# │ Runtimes: /usr/lib/owlry/runtimes/*.so Lua/Rune runtimes │
|
||||
# └─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# DMENU MODE
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Dmenu mode provides interactive selection from piped input.
|
||||
# The selected item is printed to stdout (not executed), so pipe
|
||||
# the output to execute it:
|
||||
#
|
||||
# ┌─────────────────────────────────────────────────────────────────────┐
|
||||
# │ # Screenshot menu │
|
||||
# │ printf '%s\n' \ │
|
||||
# │ "grimblast --notify copy screen" \ │
|
||||
# │ "grimblast --notify copy area" \ │
|
||||
# │ | owlry -m dmenu -p "Screenshot" \ │
|
||||
# │ | sh │
|
||||
# │ │
|
||||
# │ # Git branch checkout │
|
||||
# │ git branch | owlry -m dmenu -p "checkout" | xargs git checkout │
|
||||
# │ │
|
||||
# │ # Package search │
|
||||
# │ pacman -Ssq | owlry -m dmenu -p "install" | xargs sudo pacman -S │
|
||||
# └─────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# GENERAL
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
|
||||
[general]
|
||||
show_icons = true
|
||||
max_results = 10
|
||||
max_results = 100
|
||||
|
||||
# Terminal emulator for SSH, scripts, etc.
|
||||
# Auto-detection order: $TERMINAL → xdg-terminal-exec → DE-native → Wayland → X11 → xterm
|
||||
# Uncomment to override:
|
||||
# terminal_command = "kitty"
|
||||
|
||||
# Launch wrapper for app execution (auto-detected for uwsm/Hyprland)
|
||||
# Examples: "uwsm app --", "hyprctl dispatch exec --", ""
|
||||
# launch_wrapper = "uwsm app --"
|
||||
# Enable uwsm (Universal Wayland Session Manager) for launching apps.
|
||||
# When enabled, apps are launched via "uwsm app --" which starts them
|
||||
# in a proper systemd user session for better process management.
|
||||
# Requires: uwsm to be installed
|
||||
# use_uwsm = true
|
||||
|
||||
# Header tabs - providers shown as toggle buttons (Ctrl+1, Ctrl+2, etc.)
|
||||
# Values: app, cmd, uuctl, bookmark, calc, clip, dmenu, emoji, file, script, ssh, sys, web
|
||||
@@ -49,9 +74,8 @@ font_size = 14
|
||||
border_radius = 12
|
||||
|
||||
# Theme name - loads ~/.config/owlry/themes/{name}.css
|
||||
# Built-in: owl, catppuccin-mocha, dracula, gruvbox-dark, nord,
|
||||
# one-dark, rose-pine, solarized-dark, tokyo-night
|
||||
# Or leave unset for GTK default
|
||||
# Built-in: owl
|
||||
# Or leave unset/empty for GTK default
|
||||
# theme = "owl"
|
||||
|
||||
# Color overrides (applied on top of theme)
|
||||
@@ -63,28 +87,62 @@ border_radius = 12
|
||||
# text_secondary = "#565f89"
|
||||
# accent = "#7aa2f7"
|
||||
# accent_bright = "#89b4fa"
|
||||
#
|
||||
# Provider badge colors (optional)
|
||||
# badge_app = "#7aa2f7"
|
||||
# badge_cmd = "#9ece6a"
|
||||
# badge_bookmark = "#e0af68"
|
||||
# badge_calc = "#bb9af7"
|
||||
# badge_clip = "#7dcfff"
|
||||
# badge_dmenu = "#c0caf5"
|
||||
# badge_emoji = "#f7768e"
|
||||
# badge_file = "#73daca"
|
||||
# badge_script = "#ff9e64"
|
||||
# badge_ssh = "#2ac3de"
|
||||
# badge_sys = "#f7768e"
|
||||
# badge_uuctl = "#9ece6a"
|
||||
# badge_web = "#7aa2f7"
|
||||
# badge_media = "#bb9af7"
|
||||
# badge_weather = "#7dcfff"
|
||||
# badge_pomo = "#f7768e"
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# PLUGINS
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# All installed plugins are loaded by default. Use 'disabled' to blacklist.
|
||||
# All installed plugins are loaded by default. Use 'disabled_plugins' to blacklist.
|
||||
# Plugin IDs: calculator, system, ssh, clipboard, emoji, scripts, bookmarks,
|
||||
# websearch, filesearch, systemd, weather, media, pomodoro
|
||||
|
||||
[plugins]
|
||||
enabled = true # Master switch for all plugins
|
||||
|
||||
# Plugins to disable (by ID)
|
||||
disabled = []
|
||||
disabled_plugins = []
|
||||
|
||||
# Examples:
|
||||
# disabled = ["emoji", "pomodoro"] # Disable specific plugins
|
||||
# disabled = ["weather", "media"] # Disable widget plugins
|
||||
# disabled_plugins = ["emoji", "pomodoro"] # Disable specific plugins
|
||||
# disabled_plugins = ["weather", "media"] # Disable widget plugins
|
||||
|
||||
# Custom plugin registry URL (defaults to official registry)
|
||||
# registry_url = "https://my-registry.example.com/plugins.json"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Sandbox settings (for Lua/Rune script plugins)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# [plugins.sandbox]
|
||||
# allow_filesystem = false # Allow file system access beyond plugin dir
|
||||
# allow_network = false # Allow network requests
|
||||
# allow_commands = false # Allow shell command execution
|
||||
# memory_limit = 67108864 # Memory limit in bytes (64 MB default)
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# CORE PROVIDERS
|
||||
# PROVIDERS
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# These are built into the core binary, not plugins.
|
||||
# Enable/disable providers and configure their settings.
|
||||
# Core providers (applications, commands) are built into the binary.
|
||||
# Plugin providers require their .so to be installed.
|
||||
|
||||
[providers]
|
||||
# Core providers (always available)
|
||||
@@ -96,36 +154,41 @@ commands = true # Executables from $PATH
|
||||
frecency = true
|
||||
frecency_weight = 0.3 # 0.0 = disabled, 1.0 = strong boost
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
# PLUGIN SETTINGS
|
||||
# ═══════════════════════════════════════════════════════════════════════
|
||||
#
|
||||
# Settings for specific plugins. Only applies if the plugin is installed.
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Plugin provider toggles (require corresponding plugin installed)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
uuctl = true # systemd user units
|
||||
system = true # System commands (shutdown, reboot, etc.)
|
||||
ssh = true # SSH hosts from ~/.ssh/config
|
||||
clipboard = true # Clipboard history (requires cliphist)
|
||||
bookmarks = true # Browser bookmarks
|
||||
emoji = true # Emoji picker
|
||||
scripts = true # Custom scripts from ~/.local/share/owlry/scripts/
|
||||
files = true # File search (requires fd or mlocate)
|
||||
calculator = true # Calculator (= expression)
|
||||
websearch = true # Web search (? query)
|
||||
|
||||
# Web Search plugin
|
||||
[providers.websearch]
|
||||
search_engine = "duckduckgo"
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Widget providers (displayed at top of results)
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
media = true # MPRIS media player controls
|
||||
weather = false # Weather widget (disabled by default)
|
||||
pomodoro = false # Pomodoro timer (disabled by default)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# Provider settings
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
# Web search engine
|
||||
# Options: google, duckduckgo, bing, startpage, searxng, brave, ecosia
|
||||
# Custom URL: "https://search.example.com/?q={query}"
|
||||
# Or custom URL: "https://search.example.com/?q={query}"
|
||||
search_engine = "duckduckgo"
|
||||
|
||||
# File Search plugin
|
||||
[providers.filesearch]
|
||||
max_results = 50
|
||||
# search_paths = ["/home", "/etc"] # Custom paths (default: $HOME)
|
||||
# Weather settings (when weather = true)
|
||||
# weather_provider = "wttr.in" # Options: wttr.in, openweathermap, open-meteo
|
||||
# weather_location = "Berlin" # City name or coordinates
|
||||
# weather_api_key = "" # Required for openweathermap
|
||||
|
||||
# Weather widget plugin
|
||||
[providers.weather]
|
||||
enabled = true
|
||||
provider = "wttr.in" # wttr.in (default), openweathermap, open-meteo
|
||||
location = "" # City name, "lat,lon", or empty for auto-detect
|
||||
# api_key = "" # Required for OpenWeatherMap
|
||||
|
||||
# Pomodoro timer plugin
|
||||
[providers.pomodoro]
|
||||
enabled = true
|
||||
work_mins = 25 # Work session duration
|
||||
break_mins = 5 # Break duration
|
||||
|
||||
# Media controls plugin
|
||||
[providers.media]
|
||||
enabled = true
|
||||
# Pomodoro settings (when pomodoro = true)
|
||||
# pomodoro_work_mins = 25 # Work session duration
|
||||
# pomodoro_break_mins = 5 # Break duration
|
||||
|
||||
344
data/themes/apex-neon.css
Normal file
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Owlry - Apex Neon Theme
|
||||
* "State over Decoration."
|
||||
*
|
||||
* A high-contrast dark theme built for focus and clinical clarity.
|
||||
* Color exists to signal STATE, not to decorate space.
|
||||
*
|
||||
* Author: S0wlz (Owlibou)
|
||||
*
|
||||
* ─────────────────────────────────────────────────────────────────
|
||||
* APEX DNA - Semantic Color Roles:
|
||||
*
|
||||
* RED is the Predator: Active intent, cursor, current location, critical errors
|
||||
* CYAN is Informational: Technical data, links, neutral highlights
|
||||
* PURPLE is Sacred: Root access, special modes, exceptional states
|
||||
* GREEN is Success: Completion, OK states, positive feedback
|
||||
* YELLOW is Warning: Caution, load states, attention needed
|
||||
*
|
||||
* Rule: If a UI element is not important, it does not glow.
|
||||
* ─────────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Core Palette:
|
||||
* - Void Black: #050505 (absolute background)
|
||||
* - Dark Surface: #141414 (inputs, inactive elements)
|
||||
* - Light Surface: #262626 (separators, borders)
|
||||
* - Stark White: #ededed (primary text)
|
||||
* - Muted: #737373 (secondary text)
|
||||
* - Razor Red: #ff0044 (THE accent - focus, cursor, selection)
|
||||
* - Electric Cyan: #00eaff (info, links, technical)
|
||||
* - Sacred Purple: #9d00ff (special, root, elevated)
|
||||
* - Neon Green: #00ff99 (success, OK)
|
||||
* - Warning Yellow: #ffb700 (warning, caution)
|
||||
*
|
||||
* Bright Escalations:
|
||||
* - Alert Red: #ff8899 (distinguishable from cursor)
|
||||
* - Active Cyan: #5af3ff (active info)
|
||||
* - Active Green: #2bffb2 (active success)
|
||||
* - Urgent Yellow: #ffd24d (urgent warning)
|
||||
* - Elevated Purple:#c84dff (elevated special)
|
||||
*
|
||||
* Usage: Set theme = "apex-neon" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Core surfaces */
|
||||
--owlry-bg: #050505;
|
||||
--owlry-bg-secondary: #141414;
|
||||
--owlry-border: #262626;
|
||||
--owlry-text: #ededed;
|
||||
--owlry-text-secondary: #737373;
|
||||
|
||||
/* The Predator - primary accent */
|
||||
--owlry-accent: #ff0044;
|
||||
--owlry-accent-bright: #ff8899;
|
||||
|
||||
/* Provider badges - mapped to Apex semantics */
|
||||
--owlry-badge-app: #00eaff; /* Cyan: apps are informational */
|
||||
--owlry-badge-bookmark: #ffb700; /* Yellow: bookmarks need attention */
|
||||
--owlry-badge-calc: #ffd24d; /* Bright Yellow: calculator results */
|
||||
--owlry-badge-clip: #9d00ff; /* Purple: clipboard is special */
|
||||
--owlry-badge-cmd: #9d00ff; /* Purple: commands are elevated */
|
||||
--owlry-badge-dmenu: #00ff99; /* Green: dmenu is success/pipe */
|
||||
--owlry-badge-emoji: #c84dff; /* Bright Purple: emoji is special */
|
||||
--owlry-badge-file: #5af3ff; /* Bright Cyan: file search is active info */
|
||||
--owlry-badge-script: #2bffb2; /* Bright Green: scripts execute successfully */
|
||||
--owlry-badge-ssh: #00eaff; /* Cyan: SSH is technical/info */
|
||||
--owlry-badge-sys: #ff0044; /* Red: system actions are critical */
|
||||
--owlry-badge-uuctl: #ffb700; /* Yellow: uuctl requires attention */
|
||||
--owlry-badge-web: #00eaff; /* Cyan: web is informational */
|
||||
|
||||
/* Widget badges */
|
||||
--owlry-badge-media: #c84dff; /* Bright Purple: media is special */
|
||||
--owlry-badge-weather: #5af3ff; /* Bright Cyan: weather is active info */
|
||||
--owlry-badge-pomo: #ff8899; /* Alert Red: pomodoro demands attention */
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(5, 5, 5, 0.98);
|
||||
border: 1px solid rgba(38, 38, 38, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8),
|
||||
0 0 0 1px rgba(255, 0, 68, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(20, 20, 20, 0.9);
|
||||
border: 2px solid rgba(38, 38, 38, 0.8);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(255, 0, 68, 0.3);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(20, 20, 20, 0.8);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(255, 0, 68, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
/* Provider badges - styled per Apex semantics */
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-bookmark {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-bookmark);
|
||||
}
|
||||
|
||||
.owlry-badge-calc {
|
||||
background-color: rgba(255, 210, 77, 0.15);
|
||||
color: var(--owlry-badge-calc);
|
||||
}
|
||||
|
||||
.owlry-badge-clip {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-clip);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(0, 255, 153, 0.15);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-emoji {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-emoji);
|
||||
}
|
||||
|
||||
.owlry-badge-file {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-file);
|
||||
}
|
||||
|
||||
.owlry-badge-script {
|
||||
background-color: rgba(43, 255, 178, 0.15);
|
||||
color: var(--owlry-badge-script);
|
||||
}
|
||||
|
||||
.owlry-badge-ssh {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-ssh);
|
||||
}
|
||||
|
||||
.owlry-badge-sys {
|
||||
background-color: rgba(255, 0, 68, 0.15);
|
||||
color: var(--owlry-badge-sys);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-badge-web {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-web);
|
||||
}
|
||||
|
||||
/* Widget badges */
|
||||
.owlry-badge-media {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-media);
|
||||
}
|
||||
|
||||
.owlry-badge-weather {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-weather);
|
||||
}
|
||||
|
||||
.owlry-badge-pomo {
|
||||
background-color: rgba(255, 136, 153, 0.15);
|
||||
color: var(--owlry-badge-pomo);
|
||||
}
|
||||
|
||||
/* Filter button - default uses The Predator */
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(255, 0, 68, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(255, 0, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Provider-specific filter buttons - follow Apex semantics */
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(0, 234, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-bookmark:checked {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-bookmark);
|
||||
border-color: rgba(255, 183, 0, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-calc:checked {
|
||||
background-color: rgba(255, 210, 77, 0.15);
|
||||
color: var(--owlry-badge-calc);
|
||||
border-color: rgba(255, 210, 77, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-clip:checked {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-clip);
|
||||
border-color: rgba(157, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(157, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-dmenu:checked {
|
||||
background-color: rgba(0, 255, 153, 0.15);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
border-color: rgba(0, 255, 153, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-emoji:checked {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-emoji);
|
||||
border-color: rgba(200, 77, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-file:checked {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-file);
|
||||
border-color: rgba(90, 243, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-script:checked {
|
||||
background-color: rgba(43, 255, 178, 0.15);
|
||||
color: var(--owlry-badge-script);
|
||||
border-color: rgba(43, 255, 178, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-ssh:checked {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-ssh);
|
||||
border-color: rgba(0, 234, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-sys:checked {
|
||||
background-color: rgba(255, 0, 68, 0.15);
|
||||
color: var(--owlry-badge-sys);
|
||||
border-color: rgba(255, 0, 68, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(255, 183, 0, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-web:checked {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-web);
|
||||
border-color: rgba(0, 234, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Widget filter buttons */
|
||||
.owlry-filter-media:checked {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-media);
|
||||
border-color: rgba(200, 77, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-weather:checked {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-weather);
|
||||
border-color: rgba(90, 243, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-pomodoro:checked {
|
||||
background-color: rgba(255, 136, 153, 0.15);
|
||||
color: var(--owlry-badge-pomo);
|
||||
border-color: rgba(255, 136, 153, 0.5);
|
||||
}
|
||||
|
||||
/* Scrollbar - subtle in Void, The Predator on active */
|
||||
scrollbar slider {
|
||||
background-color: rgba(38, 38, 38, 0.8);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(64, 64, 64, 0.9);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
/* Text selection - Apex Hard Rule: black text on red (target locked) */
|
||||
selection {
|
||||
background-color: var(--owlry-accent);
|
||||
color: #050505;
|
||||
}
|
||||
|
||||
/* Mode indicator - The Predator marks current mode */
|
||||
.owlry-mode-indicator {
|
||||
background-color: rgba(255, 0, 68, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border: 1px solid rgba(255, 0, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Hints bar */
|
||||
.owlry-hints {
|
||||
border-top: 1px solid rgba(38, 38, 38, 0.8);
|
||||
}
|
||||
|
||||
.owlry-hints-label {
|
||||
color: var(--owlry-text-secondary);
|
||||
}
|
||||
|
||||
/* Tag badges in results */
|
||||
.owlry-tag-badge {
|
||||
background-color: rgba(38, 38, 38, 0.6);
|
||||
color: var(--owlry-text-secondary);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-tag-badge {
|
||||
background-color: rgba(255, 136, 153, 0.25);
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
@@ -143,9 +143,10 @@ chmod +x ~/.local/share/owlry/scripts/backup.sh
|
||||
**Prefix:** `:bm`
|
||||
**Package:** `owlry-plugin-bookmarks`
|
||||
|
||||
Browser bookmarks from Chromium-based browsers.
|
||||
Browser bookmarks from Firefox and Chromium-based browsers.
|
||||
|
||||
**Supported browsers:**
|
||||
- Firefox (reads places.sqlite)
|
||||
- Google Chrome
|
||||
- Brave
|
||||
- Microsoft Edge
|
||||
@@ -236,13 +237,7 @@ Current weather displayed at the top of results.
|
||||
- OpenWeatherMap (requires API key)
|
||||
- Open-Meteo (no API key required)
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[plugins.weather]
|
||||
provider = "wttr.in" # or: openweathermap, open-meteo
|
||||
location = "London" # city name or "lat,lon" (empty for auto-detect)
|
||||
# api_key = "..." # Required for OpenWeatherMap
|
||||
```
|
||||
**Note:** Weather configuration is currently embedded in the plugin. Future versions will support runtime configuration.
|
||||
|
||||
**Features:**
|
||||
- Temperature, condition, humidity, wind speed
|
||||
@@ -274,13 +269,6 @@ MPRIS media player controls.
|
||||
|
||||
Pomodoro timer with work/break cycles.
|
||||
|
||||
**Configuration:**
|
||||
```toml
|
||||
[plugins.pomodoro]
|
||||
work_mins = 25 # Work session duration (default: 25)
|
||||
break_mins = 5 # Break duration (default: 5)
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Configurable work session duration
|
||||
- Configurable break duration
|
||||
@@ -301,17 +289,17 @@ For convenience, plugins are available in bundle meta-packages:
|
||||
|
||||
| Bundle | Plugins |
|
||||
|--------|---------|
|
||||
| `owlry-essentials` | calculator, system, ssh, scripts, bookmarks |
|
||||
| `owlry-widgets` | weather, media, pomodoro |
|
||||
| `owlry-tools` | clipboard, emoji, websearch, filesearch, systemd |
|
||||
| `owlry-full` | All of the above |
|
||||
| `owlry-meta-essentials` | calculator, system, ssh, scripts, bookmarks |
|
||||
| `owlry-meta-widgets` | weather, media, pomodoro |
|
||||
| `owlry-meta-tools` | clipboard, emoji, websearch, filesearch, systemd |
|
||||
| `owlry-meta-full` | All of the above |
|
||||
|
||||
```bash
|
||||
# Install everything
|
||||
yay -S owlry-full
|
||||
yay -S owlry-meta-full
|
||||
|
||||
# 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]
|
||||
name = "owlry-plugin-myplugin"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
@@ -38,7 +38,7 @@ Edit `src/lib.rs`:
|
||||
use abi_stable::std_types::{ROption, RStr, RString, RVec};
|
||||
use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo,
|
||||
ProviderKind, API_VERSION,
|
||||
ProviderKind, ProviderPosition, API_VERSION,
|
||||
};
|
||||
|
||||
extern "C" fn plugin_info() -> PluginInfo {
|
||||
@@ -59,6 +59,8 @@ extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
|
||||
icon: RString::from("application-x-executable"),
|
||||
provider_type: ProviderKind::Static,
|
||||
type_id: RString::from("myplugin"),
|
||||
position: ProviderPosition::Normal,
|
||||
priority: 0, // Use frecency-based ordering
|
||||
}].into()
|
||||
}
|
||||
|
||||
@@ -198,12 +200,19 @@ pub struct ProviderInfo {
|
||||
pub icon: RString, // Default icon name
|
||||
pub provider_type: ProviderKind, // Static or Dynamic
|
||||
pub type_id: RString, // Short ID for badges
|
||||
pub position: ProviderPosition, // Normal or Widget
|
||||
pub priority: i32, // Result ordering (higher = first)
|
||||
}
|
||||
|
||||
pub enum ProviderKind {
|
||||
Static, // Items loaded at startup via refresh()
|
||||
Dynamic, // Items computed per-query via query()
|
||||
}
|
||||
|
||||
pub enum ProviderPosition {
|
||||
Normal, // Standard results (sorted by score/frecency)
|
||||
Widget, // Displayed at top when query is empty
|
||||
}
|
||||
```
|
||||
|
||||
### PluginItem
|
||||
|
||||
186
justfile
@@ -60,6 +60,18 @@ install-local:
|
||||
sudo mkdir -p /usr/lib/owlry/plugins
|
||||
sudo mkdir -p /usr/lib/owlry/runtimes
|
||||
|
||||
echo "Cleaning up stale files..."
|
||||
# Remove runtime files that may have ended up in plugins dir (from old installs)
|
||||
sudo rm -f /usr/lib/owlry/plugins/libowlry_lua.so /usr/lib/owlry/plugins/libowlry_rune.so
|
||||
# Remove old short-named plugin files (from old AUR packages before naming standardization)
|
||||
sudo rm -f /usr/lib/owlry/plugins/libbookmarks.so /usr/lib/owlry/plugins/libcalculator.so \
|
||||
/usr/lib/owlry/plugins/libclipboard.so /usr/lib/owlry/plugins/libemoji.so \
|
||||
/usr/lib/owlry/plugins/libfilesearch.so /usr/lib/owlry/plugins/libmedia.so \
|
||||
/usr/lib/owlry/plugins/libpomodoro.so /usr/lib/owlry/plugins/libscripts.so \
|
||||
/usr/lib/owlry/plugins/libssh.so /usr/lib/owlry/plugins/libsystem.so \
|
||||
/usr/lib/owlry/plugins/libsystemd.so /usr/lib/owlry/plugins/libweather.so \
|
||||
/usr/lib/owlry/plugins/libwebsearch.so
|
||||
|
||||
echo "Installing core binary..."
|
||||
sudo install -Dm755 target/release/owlry /usr/bin/owlry
|
||||
|
||||
@@ -152,6 +164,57 @@ bump-plugins new_version:
|
||||
git commit -m "chore(plugins): bump all plugins to {{new_version}}"
|
||||
echo "All plugins bumped to {{new_version}}"
|
||||
|
||||
# Bump meta-packages (no crate, just AUR version)
|
||||
bump-meta new_version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
|
||||
file="aur/$pkg/PKGBUILD"
|
||||
old=$(grep '^pkgver=' "$file" | sed 's/pkgver=//')
|
||||
if [ "$old" != "{{new_version}}" ]; then
|
||||
echo "Bumping $pkg from $old to {{new_version}}"
|
||||
sed -i 's/^pkgver=.*/pkgver={{new_version}}/' "$file"
|
||||
(cd "aur/$pkg" && makepkg --printsrcinfo > .SRCINFO)
|
||||
fi
|
||||
done
|
||||
echo "Meta-packages bumped to {{new_version}}"
|
||||
|
||||
# Bump all crates (core + plugins + runtimes) to same version
|
||||
bump-all new_version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# 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
|
||||
crate=$(basename $(dirname "$toml"))
|
||||
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
if [ "$old" != "{{new_version}}" ]; then
|
||||
echo "Bumping $crate from $old to {{new_version}}"
|
||||
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
|
||||
fi
|
||||
done
|
||||
# Bump runtimes
|
||||
for toml in crates/owlry-lua/Cargo.toml crates/owlry-rune/Cargo.toml; do
|
||||
if [ -f "$toml" ]; then
|
||||
crate=$(basename $(dirname "$toml"))
|
||||
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
if [ "$old" != "{{new_version}}" ]; then
|
||||
echo "Bumping $crate from $old to {{new_version}}"
|
||||
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cargo check --workspace
|
||||
git add crates/*/Cargo.toml Cargo.lock
|
||||
git commit -m "chore: bump all crates to {{new_version}}"
|
||||
echo "All crates bumped to {{new_version}}"
|
||||
|
||||
# Bump core version (usage: just bump 0.2.0)
|
||||
bump new_version:
|
||||
#!/usr/bin/env bash
|
||||
@@ -246,10 +309,12 @@ aur-update-pkg pkg:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine crate name (strip owlry- prefix for meta-packages)
|
||||
url="https://somegit.dev/Owlibou/owlry"
|
||||
|
||||
# Determine crate version (unified versioning: all crates share same version)
|
||||
case "{{pkg}}" in
|
||||
owlry-essentials|owlry-tools|owlry-widgets|owlry-full)
|
||||
# Meta-packages have no crate, use PKGBUILD version
|
||||
owlry-meta-essentials|owlry-meta-tools|owlry-meta-widgets|owlry-meta-full)
|
||||
# Meta-packages use static versioning (1.0.0), only bump pkgrel for dep changes
|
||||
crate_ver=$(grep '^pkgver=' "$aur_dir/PKGBUILD" | sed 's/pkgver=//')
|
||||
;;
|
||||
*)
|
||||
@@ -264,13 +329,14 @@ aur-update-pkg pkg:
|
||||
esac
|
||||
|
||||
cd "$aur_dir"
|
||||
url="https://somegit.dev/Owlibou/owlry"
|
||||
|
||||
echo "Updating {{pkg}} PKGBUILD to version $crate_ver"
|
||||
echo "Updating {{pkg}} PKGBUILD:"
|
||||
echo " pkgver=$crate_ver"
|
||||
|
||||
sed -i "s/^pkgver=.*/pkgver=$crate_ver/" PKGBUILD
|
||||
sed -i 's/^pkgrel=.*/pkgrel=1/' PKGBUILD
|
||||
|
||||
# Update checksums for packages that download source
|
||||
# Update checksums (unified versioning: all packages use same version)
|
||||
if grep -q "^source=" PKGBUILD; then
|
||||
echo "Updating checksums..."
|
||||
b2sum=$(curl -sL "$url/archive/v$crate_ver.tar.gz" | b2sum | cut -d' ' -f1)
|
||||
@@ -281,9 +347,9 @@ aur-update-pkg pkg:
|
||||
echo "Generating .SRCINFO..."
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
|
||||
git diff
|
||||
git diff --stat
|
||||
echo ""
|
||||
echo "{{pkg}} updated to $crate_ver. Run 'just aur-publish-pkg {{pkg}}' to publish."
|
||||
echo "{{pkg}} updated. Run 'just aur-publish-pkg {{pkg}}' to publish."
|
||||
|
||||
# Publish a specific AUR package
|
||||
aur-publish-pkg pkg:
|
||||
@@ -340,6 +406,16 @@ aur-publish-plugins:
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Publish all meta-packages
|
||||
aur-publish-meta:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
|
||||
echo "=== Publishing $pkg ==="
|
||||
just aur-publish-pkg "$pkg"
|
||||
done
|
||||
echo "All meta-packages published!"
|
||||
|
||||
# List all AUR packages with their versions
|
||||
aur-status:
|
||||
#!/usr/bin/env bash
|
||||
@@ -357,8 +433,60 @@ aur-status:
|
||||
fi
|
||||
done
|
||||
|
||||
# Full release workflow (bump + tag + aur)
|
||||
release-full new_version: (bump new_version)
|
||||
# Update ALL AUR packages (core + plugins + runtimes + meta)
|
||||
aur-update-all:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "=== Updating core ==="
|
||||
just aur-update
|
||||
echo ""
|
||||
echo "=== Updating plugins ==="
|
||||
for dir in aur/owlry-plugin-*/; do
|
||||
pkg=$(basename "$dir")
|
||||
echo "--- $pkg ---"
|
||||
just aur-update-pkg "$pkg"
|
||||
done
|
||||
echo ""
|
||||
echo "=== Updating runtimes ==="
|
||||
just aur-update-pkg owlry-lua
|
||||
just aur-update-pkg owlry-rune
|
||||
echo ""
|
||||
echo "=== Updating meta-packages ==="
|
||||
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
|
||||
echo "--- $pkg ---"
|
||||
(cd "aur/$pkg" && makepkg --printsrcinfo > .SRCINFO)
|
||||
done
|
||||
echo ""
|
||||
echo "All AUR packages updated. Run 'just aur-publish-all' to publish."
|
||||
|
||||
# Publish ALL AUR packages
|
||||
aur-publish-all:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "=== Publishing core ==="
|
||||
just aur-publish
|
||||
echo ""
|
||||
echo "=== Publishing plugins ==="
|
||||
for dir in aur/owlry-plugin-*/; do
|
||||
pkg=$(basename "$dir")
|
||||
echo "--- $pkg ---"
|
||||
just aur-publish-pkg "$pkg"
|
||||
done
|
||||
echo ""
|
||||
echo "=== Publishing runtimes ==="
|
||||
just aur-publish-pkg owlry-lua
|
||||
just aur-publish-pkg owlry-rune
|
||||
echo ""
|
||||
echo "=== Publishing meta-packages ==="
|
||||
for pkg in owlry-meta-essentials owlry-meta-tools owlry-meta-widgets owlry-meta-full; do
|
||||
echo "--- $pkg ---"
|
||||
just aur-publish-pkg "$pkg"
|
||||
done
|
||||
echo ""
|
||||
echo "All AUR packages published!"
|
||||
|
||||
# Full release workflow for core only (bump + tag + aur)
|
||||
release-core new_version: (bump new_version)
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
@@ -376,5 +504,41 @@ release-full new_version: (bump new_version)
|
||||
just aur-update
|
||||
|
||||
echo ""
|
||||
echo "Release v{{new_version}} prepared!"
|
||||
echo "Core release v{{new_version}} prepared!"
|
||||
echo "Review AUR changes, then run 'just aur-publish'"
|
||||
|
||||
# Full release workflow for everything (core + plugins + runtimes)
|
||||
# Usage: just release-all 0.5.0 0.3.0
|
||||
# First arg is core version, second is plugins/runtimes version
|
||||
release-all core_version plugin_version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== Bumping versions ==="
|
||||
just bump {{core_version}}
|
||||
just bump-all {{plugin_version}}
|
||||
|
||||
echo ""
|
||||
echo "=== Pushing to origin ==="
|
||||
git push
|
||||
|
||||
echo ""
|
||||
echo "=== Creating tag ==="
|
||||
just tag
|
||||
|
||||
echo "Waiting for tag to propagate..."
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
echo "=== Updating all AUR packages ==="
|
||||
just aur-update-all
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Release prepared!"
|
||||
echo " Core: v{{core_version}}"
|
||||
echo " Plugins/Runtimes: v{{plugin_version}}"
|
||||
echo ""
|
||||
echo "Review changes with 'just aur-status'"
|
||||
echo "Then publish with 'just aur-publish-all'"
|
||||
echo "=========================================="
|
||||
|
||||